Skip to content

Commit

Permalink
Sankey feature: allow nodeLabelText prop to return arbitrary HTML ele…
Browse files Browse the repository at this point in the history
…ments, to be rendered in the SVG wrapped in foreignObject tag
  • Loading branch information
Dan Delany committed Feb 21, 2018
1 parent 8a46f45 commit f477391
Showing 1 changed file with 55 additions and 17 deletions.
72 changes: 55 additions & 17 deletions src/SankeyDiagram.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,45 +88,81 @@ const SankeyNodeLabel = props => {
const getLabelText = _.isFunction(nodeLabelText) ? nodeLabelText : nodeId;
const placement = getWithNode(props.nodeLabelPlacement);
const distance = getWithNode(props.nodeLabelDistance) || 0;
const labelContent = getWithNode(getLabelText);
// don't render empty labels
if(_.isNull(labelContent) || _.isUndefined(labelContent) || labelContent === false || labelContent === "") {
return null;
}

const className = `sankey-node-label ${getWithNode(props.nodeLabelClassName)}`;
let style = {...getWithNode(props.nodeLabelStyle)};
let textPosition;
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
if (placement === "above") {
style = {alignmentBaseline: "baseline", textAnchor: "middle", ...style};
textPosition = {
// 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
};
} else if (placement === "below") {
style = {alignmentBaseline: "hanging", textAnchor: "middle", ...style};
textPosition = {
// 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
};
} else if (placement === "before") {
style = {alignmentBaseline: "middle", textAnchor: "end", ...style};
textPosition = {
// 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
};
} else {
if (!_.isUndefined(placement) && placement !== "after")
console.warn(`${placement} is not a valid value for nodeLabelPlacement - defaulting to "after"`);
style = {alignmentBaseline: "middle", textAnchor: "start", ...style};
textPosition = {
// 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
};
}

return (
<text {...textPosition} className={className} style={style}>
{getLabelText(node, graph)}
</text>
);
// if `labelContent` is a string or number, it is rendered as text within a SVG <text> element
// otherwise, it is rendered as arbitrary HTML content inside of a <foreignObject />
// 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 (
<text {...position} className={className} style={textStyle}>
{labelContent}
</text>
);

} 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 (
<foreignObject {...position} style={{overflow: "visible"}} width="5000" height="5000">
<div className={className} style={style}>
{labelContent}
</div>
</foreignObject>
);
}
};

const SankeyLinkLabel = props => {
Expand Down Expand Up @@ -428,7 +464,9 @@ export default class SankeyDiagram extends React.Component {
*/
nodeLabelDistance: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
/**
* Accessor function `nodeLabelText(node, graph)` which returns the text to be used for node labels.
* 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 `<text>`),
* or arbitrary React HTML element(s) (rendered as HTML wrapped in SVG `<foreignObject>`).
*/
nodeLabelText: PropTypes.func,
/**
Expand Down

0 comments on commit f477391

Please sign in to comment.