From 9f77123469b6dd4ee413de09280d4e0e2a8badc4 Mon Sep 17 00:00:00 2001 From: Dan Delany Date: Mon, 12 Mar 2018 14:00:05 -0400 Subject: [PATCH 1/2] SankeyDiagram SankeyNodeLabel update - if nodeLabelText returns an element, render it as SVG rather than HTML wrapped in foreignObject --- src/SankeyDiagram.js | 54 ++++++++++++++++---------------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/src/SankeyDiagram.js b/src/SankeyDiagram.js index 9d50bf0f..621bcfa1 100644 --- a/src/SankeyDiagram.js +++ b/src/SankeyDiagram.js @@ -90,21 +90,27 @@ const SankeyNodeLabel = props => { const distance = getWithNode(props.nodeLabelDistance) || 0; const labelContent = getWithNode(getLabelText); // don't render empty labels - if(_.isNull(labelContent) || _.isUndefined(labelContent) || labelContent === false || labelContent === "") { + if (_.isNull(labelContent) || _.isUndefined(labelContent) || labelContent === false || labelContent === "") { return null; } + // if `labelContent` is a string or number, it is rendered as text within a SVG element + // otherwise, it is rendered as arbitrary SVG content + // allows users to render components inside a node label (eg. to add icon or link) + const isTextLabel = _.isString(labelContent) || _.isNumber(labelContent); + if(!isTextLabel) { + return labelContent; + } + const baseClassName = `sankey-node-label ${getWithNode(props.nodeLabelClassName)}`; const baseStyle = getWithNode(props.nodeLabelStyle); let position; let textStyle; - let translate; - // use placement prop to determine x, y, alignmentBaseline and + // use placement prop to determine x, y, alignmentBaseline and textAnchor if (placement === "above") { // render label above node, centered horizontally textStyle = {alignmentBaseline: "baseline", textAnchor: "middle", ...baseStyle}; - translate = '-50%, -100%'; position = { x: node.x0 + Math.abs(node.x1 - node.x0) / 2, y: node.y0 - distance @@ -112,7 +118,6 @@ const SankeyNodeLabel = props => { } else if (placement === "below") { // render label above node, centered horizontally textStyle = {alignmentBaseline: "hanging", textAnchor: "middle", ...baseStyle}; - translate = '-50%, 0'; position = { x: node.x0 + Math.abs(node.x1 - node.x0) / 2, y: node.y1 + distance @@ -120,7 +125,6 @@ const SankeyNodeLabel = props => { } else if (placement === "before") { // render label before (to left of) node, centered vertically textStyle = {alignmentBaseline: "middle", textAnchor: "end", ...baseStyle}; - translate = '-100%, -50%'; position = { x: node.x0 - distance, y: node.y0 + Math.abs(node.y1 - node.y0) / 2 @@ -130,39 +134,18 @@ const SankeyNodeLabel = props => { console.warn(`${placement} is not a valid value for nodeLabelPlacement - defaulting to "after"`); // render label after (to right of) node, centered vertically textStyle = {alignmentBaseline: "middle", textAnchor: "start", ...baseStyle}; - translate = '0, -50%'; position = { x: node.x1 + distance, y: node.y0 + Math.abs(node.y1 - node.y0) / 2 }; } - // if `labelContent` is a string or number, it is rendered as text within a SVG element - // otherwise, it is rendered as arbitrary HTML content inside of a - // allows users to render arbitrary components inside a node label (eg. to add an icon or link) - const isTextLabel = _.isString(labelContent) || _.isNumber(labelContent); - - if(isTextLabel) { - const className = `${baseClassName} sankey-node-label-text`; - return ( - - {labelContent} - - ); - - } else { - const className = `${baseClassName} sankey-node-label-html`; - // wrap HTML labels in a div with "inline-block" so that translation (%) is relative to width of its content - const style = {...baseStyle, display: "inline-block", transform: `translate(${translate})`}; - // give foreignObject container a large width/height to prevent unintentional line breaks/cut off content - return ( - -
- {labelContent} -
-
- ); - } + const className = `${baseClassName} sankey-node-label-text`; + return ( + + {labelContent} + + ); }; const SankeyLinkLabel = props => { @@ -466,7 +449,10 @@ export default class SankeyDiagram extends React.Component { /** * Accessor function `nodeLabelText(node, graph)` which returns the content to be used for node labels. * The function may return a string/number (rendered as SVG ``), - * or arbitrary React HTML element(s) (rendered as HTML wrapped in SVG ``). + * or arbitrary React SVG element(s) (rendered as-is inside the SVG). + * NOTE: in the latter case (returning arbitrary SVG), `nodeLabelPlacement`, `nodeLabelDistance`, + * `nodeLabelClassName` and `nodeLabelStyle` props will not be applied - + * user is responsible for all positioning and attributes on this element. */ nodeLabelText: PropTypes.func, /** From 2e12b4132676fc498655e57f11dc8af0d8af545d Mon Sep 17 00:00:00 2001 From: Dan Delany Date: Mon, 12 Mar 2018 14:00:37 -0400 Subject: [PATCH 2/2] SankeyDiagram SankeyNodeLabel unit test for rendering arbitrary SVG labels --- tests/jsdom/spec/SankeyDiagram.spec.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/jsdom/spec/SankeyDiagram.spec.js b/tests/jsdom/spec/SankeyDiagram.spec.js index 1dd0536d..c915c369 100644 --- a/tests/jsdom/spec/SankeyDiagram.spec.js +++ b/tests/jsdom/spec/SankeyDiagram.spec.js @@ -610,7 +610,7 @@ describe("SankeyDiagram", () => { id: "lemons", name: "Sour Lemons" }; - it("renders a node label", () => { + it("renders a node label in a element", () => { const label = mount( "ok"}} />); const text = label.find("text"); expect(text).to.have.length(1); @@ -643,6 +643,20 @@ describe("SankeyDiagram", () => { expect(textWithId).to.have.length(1); expect(textWithId.text()).to.equal("lemons"); }); + it("renders nodeLabelText as-is (not wrapped in ), if it returns an element instead of string", () => { + const label = mount( + + }} + /> + ); + expect(label.find("text")).to.have.length(0); + expect(label.find("rect")).to.have.length(1); + expect(label.find("rect").props().width).to.equal(50); + }); + it("passes nodeLabelClassName and nodeLabelStyle through to the text element", () => { const nodeLabelClassName = "my-fun-node-label"; const nodeLabelStyle = {fill: "salmon"};