From 7344b5f523ebb7d838b480c425bd3a957076c700 Mon Sep 17 00:00:00 2001 From: Jawwad Farid Date: Tue, 11 Apr 2023 23:42:35 +0100 Subject: [PATCH 1/3] fix --- src/Node/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Node/index.tsx b/src/Node/index.tsx index 6f5acae1..1cb16397 100644 --- a/src/Node/index.tsx +++ b/src/Node/index.tsx @@ -66,15 +66,16 @@ export default class Node extends React.Component { this.commitTransform(); } - shouldComponentUpdate(nextProps: NodeProps) { - return this.shouldNodeTransform(this.props, nextProps); + shouldComponentUpdate(nextProps: NodeProps, nextState: NodeState) { + return this.shouldNodeTransform(this.props, nextProps, this.state, nextState); } - shouldNodeTransform = (ownProps: NodeProps, nextProps: NodeProps) => + shouldNodeTransform = (ownProps: NodeProps, nextProps: NodeProps, ownState: NodeState, nextState: NodeState) => nextProps.subscriptions !== ownProps.subscriptions || nextProps.position.x !== ownProps.position.x || nextProps.position.y !== ownProps.position.y || - nextProps.orientation !== ownProps.orientation; + nextProps.orientation !== ownProps.orientation || + nextState.wasClicked !== ownState.wasClicked; setTransform( position: NodeProps['position'], From 7acc5ec034fb4c8078147bfcfeca8a5548e63116 Mon Sep 17 00:00:00 2001 From: Jawwad Farid Date: Wed, 12 Apr 2023 16:00:03 +0100 Subject: [PATCH 2/3] check in build and lib folders --- .gitignore | 2 - lib/esm/Link/index.js | 103 ++++++ lib/esm/Node/DefaultNodeElement.js | 25 ++ lib/esm/Node/index.js | 113 ++++++ lib/esm/Tree/TransitionGroupWrapper.js | 4 + lib/esm/Tree/index.js | 467 +++++++++++++++++++++++++ lib/esm/Tree/types.js | 1 + lib/esm/globalCss.js | 59 ++++ lib/esm/index.js | 2 + lib/esm/types/common.js | 1 + 10 files changed, 775 insertions(+), 2 deletions(-) create mode 100644 lib/esm/Link/index.js create mode 100644 lib/esm/Node/DefaultNodeElement.js create mode 100644 lib/esm/Node/index.js create mode 100644 lib/esm/Tree/TransitionGroupWrapper.js create mode 100644 lib/esm/Tree/index.js create mode 100644 lib/esm/Tree/types.js create mode 100644 lib/esm/globalCss.js create mode 100644 lib/esm/index.js create mode 100644 lib/esm/types/common.js diff --git a/.gitignore b/.gitignore index 3d9f6c81..1644bcd1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,5 @@ node_modules stats.json # Build outputs -dist -lib demo/build demo/public/docs diff --git a/lib/esm/Link/index.js b/lib/esm/Link/index.js new file mode 100644 index 00000000..3972ecb3 --- /dev/null +++ b/lib/esm/Link/index.js @@ -0,0 +1,103 @@ +import React from 'react'; +import { linkHorizontal, linkVertical } from 'd3-shape'; +import { select } from 'd3-selection'; +export default class Link extends React.PureComponent { + constructor() { + super(...arguments); + this.linkRef = null; + this.state = { + initialStyle: { + opacity: 0, + }, + }; + this.handleOnClick = evt => { + this.props.onClick(this.props.linkData.source, this.props.linkData.target, evt); + }; + this.handleOnMouseOver = evt => { + this.props.onMouseOver(this.props.linkData.source, this.props.linkData.target, evt); + }; + this.handleOnMouseOut = evt => { + this.props.onMouseOut(this.props.linkData.source, this.props.linkData.target, evt); + }; + } + componentDidMount() { + this.applyOpacity(1, this.props.transitionDuration); + } + componentWillLeave(done) { + this.applyOpacity(0, this.props.transitionDuration, done); + } + applyOpacity(opacity, transitionDuration, done = () => { }) { + if (this.props.enableLegacyTransitions) { + select(this.linkRef) + // @ts-ignore + .transition() + .duration(transitionDuration) + .style('opacity', opacity) + .on('end', done); + } + else { + select(this.linkRef).style('opacity', opacity); + done(); + } + } + drawStepPath(linkData, orientation) { + const { source, target } = linkData; + const deltaY = target.y - source.y; + return orientation === 'horizontal' + ? `M${source.y},${source.x} H${source.y + deltaY / 2} V${target.x} H${target.y}` + : `M${source.x},${source.y} V${source.y + deltaY / 2} H${target.x} V${target.y}`; + } + drawDiagonalPath(linkData, orientation) { + const { source, target } = linkData; + return orientation === 'horizontal' + ? linkHorizontal()({ + source: [source.y, source.x], + target: [target.y, target.x], + }) + : linkVertical()({ + source: [source.x, source.y], + target: [target.x, target.y], + }); + } + drawStraightPath(linkData, orientation) { + const { source, target } = linkData; + return orientation === 'horizontal' + ? `M${source.y},${source.x}L${target.y},${target.x}` + : `M${source.x},${source.y}L${target.x},${target.y}`; + } + drawElbowPath(linkData, orientation) { + return orientation === 'horizontal' + ? `M${linkData.source.y},${linkData.source.x}V${linkData.target.x}H${linkData.target.y}` + : `M${linkData.source.x},${linkData.source.y}V${linkData.target.y}H${linkData.target.x}`; + } + drawPath() { + const { linkData, orientation, pathFunc } = this.props; + if (typeof pathFunc === 'function') { + return pathFunc(linkData, orientation); + } + if (pathFunc === 'elbow') { + return this.drawElbowPath(linkData, orientation); + } + if (pathFunc === 'straight') { + return this.drawStraightPath(linkData, orientation); + } + if (pathFunc === 'step') { + return this.drawStepPath(linkData, orientation); + } + return this.drawDiagonalPath(linkData, orientation); + } + getClassNames() { + const { linkData, orientation, pathClassFunc } = this.props; + const classNames = ['rd3t-link']; + if (typeof pathClassFunc === 'function') { + classNames.push(pathClassFunc(linkData, orientation)); + } + return classNames.join(' ').trim(); + } + render() { + const { linkData } = this.props; + return (React.createElement("path", { ref: l => { + this.linkRef = l; + }, style: Object.assign({}, this.state.initialStyle), className: this.getClassNames(), d: this.drawPath(), onClick: this.handleOnClick, onMouseOver: this.handleOnMouseOver, onMouseOut: this.handleOnMouseOut, "data-source-id": linkData.source.id, "data-target-id": linkData.target.id })); + } +} diff --git a/lib/esm/Node/DefaultNodeElement.js b/lib/esm/Node/DefaultNodeElement.js new file mode 100644 index 00000000..dc6968d6 --- /dev/null +++ b/lib/esm/Node/DefaultNodeElement.js @@ -0,0 +1,25 @@ +import React from 'react'; +const DEFAULT_NODE_CIRCLE_RADIUS = 15; +const textLayout = { + title: { + textAnchor: 'start', + x: 40, + }, + attribute: { + x: 40, + dy: '1.2em', + }, +}; +const DefaultNodeElement = ({ nodeDatum, toggleNode, onNodeClick, onNodeMouseOver, onNodeMouseOut, }) => (React.createElement(React.Fragment, null, + React.createElement("circle", { r: DEFAULT_NODE_CIRCLE_RADIUS, onClick: evt => { + toggleNode(); + onNodeClick(evt); + }, onMouseOver: onNodeMouseOver, onMouseOut: onNodeMouseOut }), + React.createElement("g", { className: "rd3t-label" }, + React.createElement("text", Object.assign({ className: "rd3t-label__title" }, textLayout.title), nodeDatum.name), + React.createElement("text", { className: "rd3t-label__attributes" }, nodeDatum.attributes && + Object.entries(nodeDatum.attributes).map(([labelKey, labelValue], i) => (React.createElement("tspan", Object.assign({ key: `${labelKey}-${i}` }, textLayout.attribute), + labelKey, + ": ", + typeof labelValue === 'boolean' ? labelValue.toString() : labelValue))))))); +export default DefaultNodeElement; diff --git a/lib/esm/Node/index.js b/lib/esm/Node/index.js new file mode 100644 index 00000000..d294e13f --- /dev/null +++ b/lib/esm/Node/index.js @@ -0,0 +1,113 @@ +import React from 'react'; +import { select } from 'd3-selection'; +import DefaultNodeElement from './DefaultNodeElement.js'; +export default class Node extends React.Component { + constructor() { + super(...arguments); + this.nodeRef = null; + this.state = { + transform: this.setTransform(this.props.position, this.props.parent, this.props.orientation, true), + initialStyle: { + opacity: 0, + }, + wasClicked: false, + }; + this.shouldNodeTransform = (ownProps, nextProps, ownState, nextState) => nextProps.subscriptions !== ownProps.subscriptions || + nextProps.position.x !== ownProps.position.x || + nextProps.position.y !== ownProps.position.y || + nextProps.orientation !== ownProps.orientation || + nextState.wasClicked !== ownState.wasClicked; + // TODO: needs tests + this.renderNodeElement = () => { + const { data, hierarchyPointNode, renderCustomNodeElement } = this.props; + const renderNode = typeof renderCustomNodeElement === 'function' ? renderCustomNodeElement : DefaultNodeElement; + const nodeProps = { + hierarchyPointNode: hierarchyPointNode, + nodeDatum: data, + toggleNode: this.handleNodeToggle, + onNodeClick: this.handleOnClick, + onNodeMouseOver: this.handleOnMouseOver, + onNodeMouseOut: this.handleOnMouseOut, + }; + return renderNode(nodeProps); + }; + this.handleNodeToggle = () => { + this.setState({ wasClicked: true }); + this.props.onNodeToggle(this.props.data.__rd3t.id); + }; + this.handleOnClick = evt => { + this.setState({ wasClicked: true }); + this.props.onNodeClick(this.props.hierarchyPointNode, evt); + }; + this.handleOnMouseOver = evt => { + this.props.onNodeMouseOver(this.props.hierarchyPointNode, evt); + }; + this.handleOnMouseOut = evt => { + this.props.onNodeMouseOut(this.props.hierarchyPointNode, evt); + }; + } + componentDidMount() { + this.commitTransform(); + } + componentDidUpdate() { + if (this.state.wasClicked) { + this.props.centerNode(this.props.hierarchyPointNode); + this.setState({ wasClicked: false }); + } + this.commitTransform(); + } + shouldComponentUpdate(nextProps, nextState) { + return this.shouldNodeTransform(this.props, nextProps, this.state, nextState); + } + setTransform(position, parent, orientation, shouldTranslateToOrigin = false) { + if (shouldTranslateToOrigin) { + const hasParent = parent !== null && parent !== undefined; + const originX = hasParent ? parent.x : 0; + const originY = hasParent ? parent.y : 0; + return orientation === 'horizontal' + ? `translate(${originY},${originX})` + : `translate(${originX},${originY})`; + } + return orientation === 'horizontal' + ? `translate(${position.y},${position.x})` + : `translate(${position.x},${position.y})`; + } + applyTransform(transform, transitionDuration, opacity = 1, done = () => { }) { + if (this.props.enableLegacyTransitions) { + select(this.nodeRef) + // @ts-ignore + .transition() + .duration(transitionDuration) + .attr('transform', transform) + .style('opacity', opacity) + .on('end', done); + } + else { + select(this.nodeRef) + .attr('transform', transform) + .style('opacity', opacity); + done(); + } + } + commitTransform() { + const { orientation, transitionDuration, position, parent } = this.props; + const transform = this.setTransform(position, parent, orientation); + this.applyTransform(transform, transitionDuration); + } + componentWillLeave(done) { + const { orientation, transitionDuration, position, parent } = this.props; + const transform = this.setTransform(position, parent, orientation, true); + this.applyTransform(transform, transitionDuration, 0, done); + } + render() { + const { data, nodeClassName } = this.props; + return (React.createElement("g", { id: data.__rd3t.id, ref: n => { + this.nodeRef = n; + }, style: this.state.initialStyle, className: [ + data.children && data.children.length > 0 ? 'rd3t-node' : 'rd3t-leaf-node', + nodeClassName, + ] + .join(' ') + .trim(), transform: this.state.transform }, this.renderNodeElement())); + } +} diff --git a/lib/esm/Tree/TransitionGroupWrapper.js b/lib/esm/Tree/TransitionGroupWrapper.js new file mode 100644 index 00000000..12ac5a1c --- /dev/null +++ b/lib/esm/Tree/TransitionGroupWrapper.js @@ -0,0 +1,4 @@ +import React from 'react'; +import { TransitionGroup } from '@bkrem/react-transition-group'; +const TransitionGroupWrapper = (props) => props.enableLegacyTransitions ? (React.createElement(TransitionGroup, { component: props.component, className: props.className, transform: props.transform }, props.children)) : (React.createElement("g", { className: props.className, transform: props.transform }, props.children)); +export default TransitionGroupWrapper; diff --git a/lib/esm/Tree/index.js b/lib/esm/Tree/index.js new file mode 100644 index 00000000..85eb1643 --- /dev/null +++ b/lib/esm/Tree/index.js @@ -0,0 +1,467 @@ +import React from 'react'; +import { tree as d3tree, hierarchy } from 'd3-hierarchy'; +import { select } from 'd3-selection'; +import { zoom as d3zoom, zoomIdentity } from 'd3-zoom'; +import { dequal as deepEqual } from 'dequal/lite'; +import clone from 'clone'; +import { v4 as uuidv4 } from 'uuid'; +import TransitionGroupWrapper from './TransitionGroupWrapper.js'; +import Node from '../Node/index.js'; +import Link from '../Link/index.js'; +import globalCss from '../globalCss.js'; +class Tree extends React.Component { + constructor() { + super(...arguments); + this.state = { + dataRef: this.props.data, + data: Tree.assignInternalProperties(clone(this.props.data)), + d3: Tree.calculateD3Geometry(this.props), + isTransitioning: false, + isInitialRenderForDataset: true, + }; + this.internalState = { + targetNode: null, + isTransitioning: false, + }; + this.svgInstanceRef = `rd3t-svg-${uuidv4()}`; + this.gInstanceRef = `rd3t-g-${uuidv4()}`; + /** + * Finds the node matching `nodeId` and + * expands/collapses it, depending on the current state of + * its internal `collapsed` property. + * `setState` callback receives targetNode and handles + * `props.onClick` if defined. + */ + this.handleNodeToggle = (nodeId) => { + const data = clone(this.state.data); + const matches = this.findNodesById(nodeId, data, []); + const targetNodeDatum = matches[0]; + if (this.props.collapsible && !this.state.isTransitioning) { + if (targetNodeDatum.__rd3t.collapsed) { + Tree.expandNode(targetNodeDatum); + this.props.shouldCollapseNeighborNodes && this.collapseNeighborNodes(targetNodeDatum, data); + } + else { + Tree.collapseNode(targetNodeDatum); + } + if (this.props.enableLegacyTransitions) { + // Lock node toggling while transition takes place. + this.setState({ data, isTransitioning: true }); + // Await transitionDuration + 10 ms before unlocking node toggling again. + setTimeout(() => this.setState({ isTransitioning: false }), this.props.transitionDuration + 10); + } + else { + this.setState({ data }); + } + this.internalState.targetNode = targetNodeDatum; + } + }; + /** + * Handles the user-defined `onNodeClick` function. + */ + this.handleOnNodeClickCb = (hierarchyPointNode, evt) => { + const { onNodeClick } = this.props; + if (onNodeClick && typeof onNodeClick === 'function') { + // Persist the SyntheticEvent for downstream handling by users. + evt.persist(); + onNodeClick(clone(hierarchyPointNode), evt); + } + }; + /** + * Handles the user-defined `onLinkClick` function. + */ + this.handleOnLinkClickCb = (linkSource, linkTarget, evt) => { + const { onLinkClick } = this.props; + if (onLinkClick && typeof onLinkClick === 'function') { + // Persist the SyntheticEvent for downstream handling by users. + evt.persist(); + onLinkClick(clone(linkSource), clone(linkTarget), evt); + } + }; + /** + * Handles the user-defined `onNodeMouseOver` function. + */ + this.handleOnNodeMouseOverCb = (hierarchyPointNode, evt) => { + const { onNodeMouseOver } = this.props; + if (onNodeMouseOver && typeof onNodeMouseOver === 'function') { + // Persist the SyntheticEvent for downstream handling by users. + evt.persist(); + onNodeMouseOver(clone(hierarchyPointNode), evt); + } + }; + /** + * Handles the user-defined `onLinkMouseOver` function. + */ + this.handleOnLinkMouseOverCb = (linkSource, linkTarget, evt) => { + const { onLinkMouseOver } = this.props; + if (onLinkMouseOver && typeof onLinkMouseOver === 'function') { + // Persist the SyntheticEvent for downstream handling by users. + evt.persist(); + onLinkMouseOver(clone(linkSource), clone(linkTarget), evt); + } + }; + /** + * Handles the user-defined `onNodeMouseOut` function. + */ + this.handleOnNodeMouseOutCb = (hierarchyPointNode, evt) => { + const { onNodeMouseOut } = this.props; + if (onNodeMouseOut && typeof onNodeMouseOut === 'function') { + // Persist the SyntheticEvent for downstream handling by users. + evt.persist(); + onNodeMouseOut(clone(hierarchyPointNode), evt); + } + }; + /** + * Handles the user-defined `onLinkMouseOut` function. + */ + this.handleOnLinkMouseOutCb = (linkSource, linkTarget, evt) => { + const { onLinkMouseOut } = this.props; + if (onLinkMouseOut && typeof onLinkMouseOut === 'function') { + // Persist the SyntheticEvent for downstream handling by users. + evt.persist(); + onLinkMouseOut(clone(linkSource), clone(linkTarget), evt); + } + }; + /** + * Takes a hierarchy point node and centers the node on the screen + * if the dimensions parameter is passed to `Tree`. + * + * This code is adapted from Rob Schmuecker's centerNode method. + * Link: http://bl.ocks.org/robschmuecker/7880033 + */ + this.centerNode = (hierarchyPointNode) => { + const { dimensions, orientation, zoom, centeringTransitionDuration } = this.props; + if (dimensions) { + const g = select(`.${this.gInstanceRef}`); + const svg = select(`.${this.svgInstanceRef}`); + const scale = this.state.d3.scale; + let x; + let y; + // if the orientation is horizontal, calculate the variables inverted (x->y, y->x) + if (orientation === 'horizontal') { + y = -hierarchyPointNode.x * scale + dimensions.height / 2; + x = -hierarchyPointNode.y * scale + dimensions.width / 2; + } + else { + // else, calculate the variables normally (x->x, y->y) + x = -hierarchyPointNode.x * scale + dimensions.width / 2; + y = -hierarchyPointNode.y * scale + dimensions.height / 2; + } + //@ts-ignore + g.transition() + .duration(centeringTransitionDuration) + .attr('transform', 'translate(' + x + ',' + y + ')scale(' + scale + ')'); + // Sets the viewport to the new center so that it does not jump back to original + // coordinates when dragged/zoomed + //@ts-ignore + svg.call(d3zoom().transform, zoomIdentity.translate(x, y).scale(zoom)); + } + }; + /** + * Determines which additional `className` prop should be passed to the node & returns it. + */ + this.getNodeClassName = (parent, nodeDatum) => { + const { rootNodeClassName, branchNodeClassName, leafNodeClassName } = this.props; + const hasParent = parent !== null && parent !== undefined; + if (hasParent) { + return nodeDatum.children ? branchNodeClassName : leafNodeClassName; + } + else { + return rootNodeClassName; + } + }; + } + static getDerivedStateFromProps(nextProps, prevState) { + let derivedState = null; + // Clone new data & assign internal properties if `data` object reference changed. + if (nextProps.data !== prevState.dataRef) { + derivedState = { + dataRef: nextProps.data, + data: Tree.assignInternalProperties(clone(nextProps.data)), + isInitialRenderForDataset: true, + }; + } + const d3 = Tree.calculateD3Geometry(nextProps); + if (!deepEqual(d3, prevState.d3)) { + derivedState = derivedState || {}; + derivedState.d3 = d3; + } + return derivedState; + } + componentDidMount() { + this.bindZoomListener(this.props); + this.setState({ isInitialRenderForDataset: false }); + } + componentDidUpdate(prevProps) { + if (this.props.data !== prevProps.data) { + // If last `render` was due to change in dataset -> mark the initial render as done. + this.setState({ isInitialRenderForDataset: false }); + } + if (!deepEqual(this.props.translate, prevProps.translate) || + !deepEqual(this.props.scaleExtent, prevProps.scaleExtent) || + this.props.zoomable !== prevProps.zoomable || + this.props.draggable !== prevProps.draggable || + this.props.zoom !== prevProps.zoom || + this.props.enableLegacyTransitions !== prevProps.enableLegacyTransitions) { + // If zoom-specific props change -> rebind listener with new values. + // Or: rebind zoom listeners to new DOM nodes in case legacy transitions were enabled/disabled. + this.bindZoomListener(this.props); + } + if (typeof this.props.onUpdate === 'function') { + this.props.onUpdate({ + node: this.internalState.targetNode ? clone(this.internalState.targetNode) : null, + zoom: this.state.d3.scale, + translate: this.state.d3.translate, + }); + } + // Reset the last target node after we've flushed it to `onUpdate`. + this.internalState.targetNode = null; + } + /** + * Collapses all tree nodes with a `depth` larger than `initialDepth`. + * + * @param {array} nodeSet Array of nodes generated by `generateTree` + * @param {number} initialDepth Maximum initial depth the tree should render + */ + setInitialTreeDepth(nodeSet, initialDepth) { + nodeSet.forEach(n => { + n.data.__rd3t.collapsed = n.depth >= initialDepth; + }); + } + /** + * bindZoomListener - If `props.zoomable`, binds a listener for + * "zoom" events to the SVG and sets scaleExtent to min/max + * specified in `props.scaleExtent`. + */ + bindZoomListener(props) { + const { zoomable, scaleExtent, translate, zoom, onUpdate, hasInteractiveNodes } = props; + const svg = select(`.${this.svgInstanceRef}`); + const g = select(`.${this.gInstanceRef}`); + // Sets initial offset, so that first pan and zoom does not jump back to default [0,0] coords. + // @ts-ignore + svg.call(d3zoom().transform, zoomIdentity.translate(translate.x, translate.y).scale(zoom)); + svg.call(d3zoom() + .scaleExtent(zoomable ? [scaleExtent.min, scaleExtent.max] : [zoom, zoom]) + // TODO: break this out into a separate zoom handler fn, rather than inlining it. + .filter((event) => { + if (hasInteractiveNodes) { + return (event.target.classList.contains(this.svgInstanceRef) || + event.target.classList.contains(this.gInstanceRef) || + event.shiftKey); + } + return true; + }) + .on('zoom', (event) => { + if (!this.props.draggable && + (event.sourceEvent.type === 'mousemove' || event.sourceEvent.type === 'touchmove')) { + return; + } + g.attr('transform', event.transform); + if (typeof onUpdate === 'function') { + // This callback is magically called not only on "zoom", but on "drag", as well, + // even though event.type == "zoom". + // Taking advantage of this and not writing a "drag" handler. + onUpdate({ + node: null, + zoom: event.transform.k, + translate: { x: event.transform.x, y: event.transform.y }, + }); + // TODO: remove this? Shouldn't be mutating state keys directly. + this.state.d3.scale = event.transform.k; + this.state.d3.translate = { + x: event.transform.x, + y: event.transform.y, + }; + } + })); + } + /** + * Assigns internal properties that are required for tree + * manipulation to each node in the `data` set and returns a new `data` array. + * + * @static + */ + static assignInternalProperties(data, currentDepth = 0) { + // Wrap the root node into an array for recursive transformations if it wasn't in one already. + const d = Array.isArray(data) ? data : [data]; + return d.map(n => { + const nodeDatum = n; + nodeDatum.__rd3t = { id: null, depth: null, collapsed: false }; + nodeDatum.__rd3t.id = uuidv4(); + // D3@v5 compat: manually assign `depth` to node.data so we don't have + // to hold full node+link sets in state. + // TODO: avoid this extra step by checking D3's node.depth directly. + nodeDatum.__rd3t.depth = currentDepth; + // If there are children, recursively assign properties to them too. + if (nodeDatum.children && nodeDatum.children.length > 0) { + nodeDatum.children = Tree.assignInternalProperties(nodeDatum.children, currentDepth + 1); + } + return nodeDatum; + }); + } + /** + * Recursively walks the nested `nodeSet` until a node matching `nodeId` is found. + */ + findNodesById(nodeId, nodeSet, hits) { + if (hits.length > 0) { + return hits; + } + hits = hits.concat(nodeSet.filter(node => node.__rd3t.id === nodeId)); + nodeSet.forEach(node => { + if (node.children && node.children.length > 0) { + hits = this.findNodesById(nodeId, node.children, hits); + } + }); + return hits; + } + /** + * Recursively walks the nested `nodeSet` until all nodes at `depth` have been found. + * + * @param {number} depth Target depth for which nodes should be returned + * @param {array} nodeSet Array of nested `node` objects + * @param {array} accumulator Accumulator for matches, passed between recursive calls + */ + findNodesAtDepth(depth, nodeSet, accumulator) { + accumulator = accumulator.concat(nodeSet.filter(node => node.__rd3t.depth === depth)); + nodeSet.forEach(node => { + if (node.children && node.children.length > 0) { + accumulator = this.findNodesAtDepth(depth, node.children, accumulator); + } + }); + return accumulator; + } + /** + * Recursively sets the internal `collapsed` property of + * the passed `TreeNodeDatum` and its children to `true`. + * + * @static + */ + static collapseNode(nodeDatum) { + nodeDatum.__rd3t.collapsed = true; + if (nodeDatum.children && nodeDatum.children.length > 0) { + nodeDatum.children.forEach(child => { + Tree.collapseNode(child); + }); + } + } + /** + * Sets the internal `collapsed` property of + * the passed `TreeNodeDatum` object to `false`. + * + * @static + */ + static expandNode(nodeDatum) { + nodeDatum.__rd3t.collapsed = false; + } + /** + * Collapses all nodes in `nodeSet` that are neighbors (same depth) of `targetNode`. + */ + collapseNeighborNodes(targetNode, nodeSet) { + const neighbors = this.findNodesAtDepth(targetNode.__rd3t.depth, nodeSet, []).filter(node => node.__rd3t.id !== targetNode.__rd3t.id); + neighbors.forEach(neighbor => Tree.collapseNode(neighbor)); + } + /** + * Generates tree elements (`nodes` and `links`) by + * grabbing the rootNode from `this.state.data[0]`. + * Restricts tree depth to `props.initialDepth` if defined and if this is + * the initial render of the tree. + */ + generateTree() { + const { initialDepth, depthFactor, separation, nodeSize, orientation } = this.props; + const { isInitialRenderForDataset } = this.state; + const tree = d3tree() + .nodeSize(orientation === 'horizontal' ? [nodeSize.y, nodeSize.x] : [nodeSize.x, nodeSize.y]) + .separation((a, b) => a.parent.data.__rd3t.id === b.parent.data.__rd3t.id + ? separation.siblings + : separation.nonSiblings); + const rootNode = tree(hierarchy(this.state.data[0], d => (d.__rd3t.collapsed ? null : d.children))); + let nodes = rootNode.descendants(); + const links = rootNode.links(); + // Configure nodes' `collapsed` property on first render if `initialDepth` is defined. + if (initialDepth !== undefined && isInitialRenderForDataset) { + this.setInitialTreeDepth(nodes, initialDepth); + } + if (depthFactor) { + nodes.forEach(node => { + node.y = node.depth * depthFactor; + }); + } + return { nodes, links }; + } + /** + * Set initial zoom and position. + * Also limit zoom level according to `scaleExtent` on initial display. This is necessary, + * because the first time we are setting it as an SVG property, instead of going + * through D3's scaling mechanism, which would have picked up both properties. + * + * @static + */ + static calculateD3Geometry(nextProps) { + let scale; + if (nextProps.zoom > nextProps.scaleExtent.max) { + scale = nextProps.scaleExtent.max; + } + else if (nextProps.zoom < nextProps.scaleExtent.min) { + scale = nextProps.scaleExtent.min; + } + else { + scale = nextProps.zoom; + } + return { + translate: nextProps.translate, + scale, + }; + } + render() { + const { nodes, links } = this.generateTree(); + const { renderCustomNodeElement, orientation, pathFunc, transitionDuration, nodeSize, depthFactor, initialDepth, separation, enableLegacyTransitions, svgClassName, pathClassFunc, } = this.props; + const { translate, scale } = this.state.d3; + const subscriptions = Object.assign(Object.assign(Object.assign({}, nodeSize), separation), { depthFactor, + initialDepth }); + return (React.createElement("div", { className: "rd3t-tree-container rd3t-grabbable" }, + React.createElement("style", null, globalCss), + React.createElement("svg", { className: `rd3t-svg ${this.svgInstanceRef} ${svgClassName}`, width: "100%", height: "100%" }, + React.createElement(TransitionGroupWrapper, { enableLegacyTransitions: enableLegacyTransitions, component: "g", className: `rd3t-g ${this.gInstanceRef}`, transform: `translate(${translate.x},${translate.y}) scale(${scale})` }, + links.map((linkData, i) => { + return (React.createElement(Link, { key: 'link-' + i, orientation: orientation, pathFunc: pathFunc, pathClassFunc: pathClassFunc, linkData: linkData, onClick: this.handleOnLinkClickCb, onMouseOver: this.handleOnLinkMouseOverCb, onMouseOut: this.handleOnLinkMouseOutCb, enableLegacyTransitions: enableLegacyTransitions, transitionDuration: transitionDuration })); + }), + nodes.map((hierarchyPointNode, i) => { + const { data, x, y, parent } = hierarchyPointNode; + return (React.createElement(Node, { key: 'node-' + i, data: data, position: { x, y }, hierarchyPointNode: hierarchyPointNode, parent: parent, nodeClassName: this.getNodeClassName(parent, data), renderCustomNodeElement: renderCustomNodeElement, nodeSize: nodeSize, orientation: orientation, enableLegacyTransitions: enableLegacyTransitions, transitionDuration: transitionDuration, onNodeToggle: this.handleNodeToggle, onNodeClick: this.handleOnNodeClickCb, onNodeMouseOver: this.handleOnNodeMouseOverCb, onNodeMouseOut: this.handleOnNodeMouseOutCb, subscriptions: subscriptions, centerNode: this.centerNode })); + }))))); + } +} +Tree.defaultProps = { + onNodeClick: undefined, + onNodeMouseOver: undefined, + onNodeMouseOut: undefined, + onLinkClick: undefined, + onLinkMouseOver: undefined, + onLinkMouseOut: undefined, + onUpdate: undefined, + orientation: 'horizontal', + translate: { x: 0, y: 0 }, + pathFunc: 'diagonal', + pathClassFunc: undefined, + transitionDuration: 500, + depthFactor: undefined, + collapsible: true, + initialDepth: undefined, + zoomable: true, + draggable: true, + zoom: 1, + scaleExtent: { min: 0.1, max: 1 }, + nodeSize: { x: 140, y: 140 }, + separation: { siblings: 1, nonSiblings: 2 }, + shouldCollapseNeighborNodes: false, + svgClassName: '', + rootNodeClassName: '', + branchNodeClassName: '', + leafNodeClassName: '', + renderCustomNodeElement: undefined, + enableLegacyTransitions: false, + hasInteractiveNodes: false, + dimensions: undefined, + centeringTransitionDuration: 800, +}; +export default Tree; diff --git a/lib/esm/Tree/types.js b/lib/esm/Tree/types.js new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/lib/esm/Tree/types.js @@ -0,0 +1 @@ +export {}; diff --git a/lib/esm/globalCss.js b/lib/esm/globalCss.js new file mode 100644 index 00000000..134728a2 --- /dev/null +++ b/lib/esm/globalCss.js @@ -0,0 +1,59 @@ +// Importing CSS files globally (e.g. `import "./styles.css"`) can cause resolution issues with certain +// libraries/frameworks. +// Example: Next.js (https://github.com/vercel/next.js/blob/master/errors/css-npm.md) +// +// Since rd3t's CSS is bare bones to begin with, we provide all required styles as a template string, +// which can be imported like any other TS/JS module and inlined into a `` tag. +export default ` +/* Tree */ +.rd3t-tree-container { + width: 100%; + height: 100%; +} + +.rd3t-grabbable { + cursor: move; /* fallback if grab cursor is unsupported */ + cursor: grab; + cursor: -moz-grab; + cursor: -webkit-grab; +} +.rd3t-grabbable:active { + cursor: grabbing; + cursor: -moz-grabbing; + cursor: -webkit-grabbing; +} + +/* Node */ +.rd3t-node { + cursor: pointer; + fill: #777; + stroke: #000; + stroke-width: 2; +} + +.rd3t-leaf-node { + cursor: pointer; + fill: transparent; + stroke: #000; + stroke-width: 1; +} + +.rd3t-label__title { + fill: #000; + stroke: none; + font-weight: bolder; +} + +.rd3t-label__attributes { + fill: #777; + stroke: none; + font-weight: bolder; + font-size: smaller; +} + +/* Link */ +.rd3t-link { + fill: none; + stroke: #000; +} +`; diff --git a/lib/esm/index.js b/lib/esm/index.js new file mode 100644 index 00000000..e8f3a713 --- /dev/null +++ b/lib/esm/index.js @@ -0,0 +1,2 @@ +import Tree from './Tree/index.js'; +export default Tree; diff --git a/lib/esm/types/common.js b/lib/esm/types/common.js new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/lib/esm/types/common.js @@ -0,0 +1 @@ +export {}; From c9f9c8b12bc747a75d3dcabe8d0cdf5ec75bd632 Mon Sep 17 00:00:00 2001 From: Jawwad Farid Date: Wed, 12 Apr 2023 16:17:46 +0100 Subject: [PATCH 3/3] Revert "check in build and lib folders" This reverts commit 7acc5ec034fb4c8078147bfcfeca8a5548e63116. --- .gitignore | 2 + lib/esm/Link/index.js | 103 ------ lib/esm/Node/DefaultNodeElement.js | 25 -- lib/esm/Node/index.js | 113 ------ lib/esm/Tree/TransitionGroupWrapper.js | 4 - lib/esm/Tree/index.js | 467 ------------------------- lib/esm/Tree/types.js | 1 - lib/esm/globalCss.js | 59 ---- lib/esm/index.js | 2 - lib/esm/types/common.js | 1 - 10 files changed, 2 insertions(+), 775 deletions(-) delete mode 100644 lib/esm/Link/index.js delete mode 100644 lib/esm/Node/DefaultNodeElement.js delete mode 100644 lib/esm/Node/index.js delete mode 100644 lib/esm/Tree/TransitionGroupWrapper.js delete mode 100644 lib/esm/Tree/index.js delete mode 100644 lib/esm/Tree/types.js delete mode 100644 lib/esm/globalCss.js delete mode 100644 lib/esm/index.js delete mode 100644 lib/esm/types/common.js diff --git a/.gitignore b/.gitignore index 1644bcd1..3d9f6c81 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,7 @@ node_modules stats.json # Build outputs +dist +lib demo/build demo/public/docs diff --git a/lib/esm/Link/index.js b/lib/esm/Link/index.js deleted file mode 100644 index 3972ecb3..00000000 --- a/lib/esm/Link/index.js +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; -import { linkHorizontal, linkVertical } from 'd3-shape'; -import { select } from 'd3-selection'; -export default class Link extends React.PureComponent { - constructor() { - super(...arguments); - this.linkRef = null; - this.state = { - initialStyle: { - opacity: 0, - }, - }; - this.handleOnClick = evt => { - this.props.onClick(this.props.linkData.source, this.props.linkData.target, evt); - }; - this.handleOnMouseOver = evt => { - this.props.onMouseOver(this.props.linkData.source, this.props.linkData.target, evt); - }; - this.handleOnMouseOut = evt => { - this.props.onMouseOut(this.props.linkData.source, this.props.linkData.target, evt); - }; - } - componentDidMount() { - this.applyOpacity(1, this.props.transitionDuration); - } - componentWillLeave(done) { - this.applyOpacity(0, this.props.transitionDuration, done); - } - applyOpacity(opacity, transitionDuration, done = () => { }) { - if (this.props.enableLegacyTransitions) { - select(this.linkRef) - // @ts-ignore - .transition() - .duration(transitionDuration) - .style('opacity', opacity) - .on('end', done); - } - else { - select(this.linkRef).style('opacity', opacity); - done(); - } - } - drawStepPath(linkData, orientation) { - const { source, target } = linkData; - const deltaY = target.y - source.y; - return orientation === 'horizontal' - ? `M${source.y},${source.x} H${source.y + deltaY / 2} V${target.x} H${target.y}` - : `M${source.x},${source.y} V${source.y + deltaY / 2} H${target.x} V${target.y}`; - } - drawDiagonalPath(linkData, orientation) { - const { source, target } = linkData; - return orientation === 'horizontal' - ? linkHorizontal()({ - source: [source.y, source.x], - target: [target.y, target.x], - }) - : linkVertical()({ - source: [source.x, source.y], - target: [target.x, target.y], - }); - } - drawStraightPath(linkData, orientation) { - const { source, target } = linkData; - return orientation === 'horizontal' - ? `M${source.y},${source.x}L${target.y},${target.x}` - : `M${source.x},${source.y}L${target.x},${target.y}`; - } - drawElbowPath(linkData, orientation) { - return orientation === 'horizontal' - ? `M${linkData.source.y},${linkData.source.x}V${linkData.target.x}H${linkData.target.y}` - : `M${linkData.source.x},${linkData.source.y}V${linkData.target.y}H${linkData.target.x}`; - } - drawPath() { - const { linkData, orientation, pathFunc } = this.props; - if (typeof pathFunc === 'function') { - return pathFunc(linkData, orientation); - } - if (pathFunc === 'elbow') { - return this.drawElbowPath(linkData, orientation); - } - if (pathFunc === 'straight') { - return this.drawStraightPath(linkData, orientation); - } - if (pathFunc === 'step') { - return this.drawStepPath(linkData, orientation); - } - return this.drawDiagonalPath(linkData, orientation); - } - getClassNames() { - const { linkData, orientation, pathClassFunc } = this.props; - const classNames = ['rd3t-link']; - if (typeof pathClassFunc === 'function') { - classNames.push(pathClassFunc(linkData, orientation)); - } - return classNames.join(' ').trim(); - } - render() { - const { linkData } = this.props; - return (React.createElement("path", { ref: l => { - this.linkRef = l; - }, style: Object.assign({}, this.state.initialStyle), className: this.getClassNames(), d: this.drawPath(), onClick: this.handleOnClick, onMouseOver: this.handleOnMouseOver, onMouseOut: this.handleOnMouseOut, "data-source-id": linkData.source.id, "data-target-id": linkData.target.id })); - } -} diff --git a/lib/esm/Node/DefaultNodeElement.js b/lib/esm/Node/DefaultNodeElement.js deleted file mode 100644 index dc6968d6..00000000 --- a/lib/esm/Node/DefaultNodeElement.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -const DEFAULT_NODE_CIRCLE_RADIUS = 15; -const textLayout = { - title: { - textAnchor: 'start', - x: 40, - }, - attribute: { - x: 40, - dy: '1.2em', - }, -}; -const DefaultNodeElement = ({ nodeDatum, toggleNode, onNodeClick, onNodeMouseOver, onNodeMouseOut, }) => (React.createElement(React.Fragment, null, - React.createElement("circle", { r: DEFAULT_NODE_CIRCLE_RADIUS, onClick: evt => { - toggleNode(); - onNodeClick(evt); - }, onMouseOver: onNodeMouseOver, onMouseOut: onNodeMouseOut }), - React.createElement("g", { className: "rd3t-label" }, - React.createElement("text", Object.assign({ className: "rd3t-label__title" }, textLayout.title), nodeDatum.name), - React.createElement("text", { className: "rd3t-label__attributes" }, nodeDatum.attributes && - Object.entries(nodeDatum.attributes).map(([labelKey, labelValue], i) => (React.createElement("tspan", Object.assign({ key: `${labelKey}-${i}` }, textLayout.attribute), - labelKey, - ": ", - typeof labelValue === 'boolean' ? labelValue.toString() : labelValue))))))); -export default DefaultNodeElement; diff --git a/lib/esm/Node/index.js b/lib/esm/Node/index.js deleted file mode 100644 index d294e13f..00000000 --- a/lib/esm/Node/index.js +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; -import { select } from 'd3-selection'; -import DefaultNodeElement from './DefaultNodeElement.js'; -export default class Node extends React.Component { - constructor() { - super(...arguments); - this.nodeRef = null; - this.state = { - transform: this.setTransform(this.props.position, this.props.parent, this.props.orientation, true), - initialStyle: { - opacity: 0, - }, - wasClicked: false, - }; - this.shouldNodeTransform = (ownProps, nextProps, ownState, nextState) => nextProps.subscriptions !== ownProps.subscriptions || - nextProps.position.x !== ownProps.position.x || - nextProps.position.y !== ownProps.position.y || - nextProps.orientation !== ownProps.orientation || - nextState.wasClicked !== ownState.wasClicked; - // TODO: needs tests - this.renderNodeElement = () => { - const { data, hierarchyPointNode, renderCustomNodeElement } = this.props; - const renderNode = typeof renderCustomNodeElement === 'function' ? renderCustomNodeElement : DefaultNodeElement; - const nodeProps = { - hierarchyPointNode: hierarchyPointNode, - nodeDatum: data, - toggleNode: this.handleNodeToggle, - onNodeClick: this.handleOnClick, - onNodeMouseOver: this.handleOnMouseOver, - onNodeMouseOut: this.handleOnMouseOut, - }; - return renderNode(nodeProps); - }; - this.handleNodeToggle = () => { - this.setState({ wasClicked: true }); - this.props.onNodeToggle(this.props.data.__rd3t.id); - }; - this.handleOnClick = evt => { - this.setState({ wasClicked: true }); - this.props.onNodeClick(this.props.hierarchyPointNode, evt); - }; - this.handleOnMouseOver = evt => { - this.props.onNodeMouseOver(this.props.hierarchyPointNode, evt); - }; - this.handleOnMouseOut = evt => { - this.props.onNodeMouseOut(this.props.hierarchyPointNode, evt); - }; - } - componentDidMount() { - this.commitTransform(); - } - componentDidUpdate() { - if (this.state.wasClicked) { - this.props.centerNode(this.props.hierarchyPointNode); - this.setState({ wasClicked: false }); - } - this.commitTransform(); - } - shouldComponentUpdate(nextProps, nextState) { - return this.shouldNodeTransform(this.props, nextProps, this.state, nextState); - } - setTransform(position, parent, orientation, shouldTranslateToOrigin = false) { - if (shouldTranslateToOrigin) { - const hasParent = parent !== null && parent !== undefined; - const originX = hasParent ? parent.x : 0; - const originY = hasParent ? parent.y : 0; - return orientation === 'horizontal' - ? `translate(${originY},${originX})` - : `translate(${originX},${originY})`; - } - return orientation === 'horizontal' - ? `translate(${position.y},${position.x})` - : `translate(${position.x},${position.y})`; - } - applyTransform(transform, transitionDuration, opacity = 1, done = () => { }) { - if (this.props.enableLegacyTransitions) { - select(this.nodeRef) - // @ts-ignore - .transition() - .duration(transitionDuration) - .attr('transform', transform) - .style('opacity', opacity) - .on('end', done); - } - else { - select(this.nodeRef) - .attr('transform', transform) - .style('opacity', opacity); - done(); - } - } - commitTransform() { - const { orientation, transitionDuration, position, parent } = this.props; - const transform = this.setTransform(position, parent, orientation); - this.applyTransform(transform, transitionDuration); - } - componentWillLeave(done) { - const { orientation, transitionDuration, position, parent } = this.props; - const transform = this.setTransform(position, parent, orientation, true); - this.applyTransform(transform, transitionDuration, 0, done); - } - render() { - const { data, nodeClassName } = this.props; - return (React.createElement("g", { id: data.__rd3t.id, ref: n => { - this.nodeRef = n; - }, style: this.state.initialStyle, className: [ - data.children && data.children.length > 0 ? 'rd3t-node' : 'rd3t-leaf-node', - nodeClassName, - ] - .join(' ') - .trim(), transform: this.state.transform }, this.renderNodeElement())); - } -} diff --git a/lib/esm/Tree/TransitionGroupWrapper.js b/lib/esm/Tree/TransitionGroupWrapper.js deleted file mode 100644 index 12ac5a1c..00000000 --- a/lib/esm/Tree/TransitionGroupWrapper.js +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; -import { TransitionGroup } from '@bkrem/react-transition-group'; -const TransitionGroupWrapper = (props) => props.enableLegacyTransitions ? (React.createElement(TransitionGroup, { component: props.component, className: props.className, transform: props.transform }, props.children)) : (React.createElement("g", { className: props.className, transform: props.transform }, props.children)); -export default TransitionGroupWrapper; diff --git a/lib/esm/Tree/index.js b/lib/esm/Tree/index.js deleted file mode 100644 index 85eb1643..00000000 --- a/lib/esm/Tree/index.js +++ /dev/null @@ -1,467 +0,0 @@ -import React from 'react'; -import { tree as d3tree, hierarchy } from 'd3-hierarchy'; -import { select } from 'd3-selection'; -import { zoom as d3zoom, zoomIdentity } from 'd3-zoom'; -import { dequal as deepEqual } from 'dequal/lite'; -import clone from 'clone'; -import { v4 as uuidv4 } from 'uuid'; -import TransitionGroupWrapper from './TransitionGroupWrapper.js'; -import Node from '../Node/index.js'; -import Link from '../Link/index.js'; -import globalCss from '../globalCss.js'; -class Tree extends React.Component { - constructor() { - super(...arguments); - this.state = { - dataRef: this.props.data, - data: Tree.assignInternalProperties(clone(this.props.data)), - d3: Tree.calculateD3Geometry(this.props), - isTransitioning: false, - isInitialRenderForDataset: true, - }; - this.internalState = { - targetNode: null, - isTransitioning: false, - }; - this.svgInstanceRef = `rd3t-svg-${uuidv4()}`; - this.gInstanceRef = `rd3t-g-${uuidv4()}`; - /** - * Finds the node matching `nodeId` and - * expands/collapses it, depending on the current state of - * its internal `collapsed` property. - * `setState` callback receives targetNode and handles - * `props.onClick` if defined. - */ - this.handleNodeToggle = (nodeId) => { - const data = clone(this.state.data); - const matches = this.findNodesById(nodeId, data, []); - const targetNodeDatum = matches[0]; - if (this.props.collapsible && !this.state.isTransitioning) { - if (targetNodeDatum.__rd3t.collapsed) { - Tree.expandNode(targetNodeDatum); - this.props.shouldCollapseNeighborNodes && this.collapseNeighborNodes(targetNodeDatum, data); - } - else { - Tree.collapseNode(targetNodeDatum); - } - if (this.props.enableLegacyTransitions) { - // Lock node toggling while transition takes place. - this.setState({ data, isTransitioning: true }); - // Await transitionDuration + 10 ms before unlocking node toggling again. - setTimeout(() => this.setState({ isTransitioning: false }), this.props.transitionDuration + 10); - } - else { - this.setState({ data }); - } - this.internalState.targetNode = targetNodeDatum; - } - }; - /** - * Handles the user-defined `onNodeClick` function. - */ - this.handleOnNodeClickCb = (hierarchyPointNode, evt) => { - const { onNodeClick } = this.props; - if (onNodeClick && typeof onNodeClick === 'function') { - // Persist the SyntheticEvent for downstream handling by users. - evt.persist(); - onNodeClick(clone(hierarchyPointNode), evt); - } - }; - /** - * Handles the user-defined `onLinkClick` function. - */ - this.handleOnLinkClickCb = (linkSource, linkTarget, evt) => { - const { onLinkClick } = this.props; - if (onLinkClick && typeof onLinkClick === 'function') { - // Persist the SyntheticEvent for downstream handling by users. - evt.persist(); - onLinkClick(clone(linkSource), clone(linkTarget), evt); - } - }; - /** - * Handles the user-defined `onNodeMouseOver` function. - */ - this.handleOnNodeMouseOverCb = (hierarchyPointNode, evt) => { - const { onNodeMouseOver } = this.props; - if (onNodeMouseOver && typeof onNodeMouseOver === 'function') { - // Persist the SyntheticEvent for downstream handling by users. - evt.persist(); - onNodeMouseOver(clone(hierarchyPointNode), evt); - } - }; - /** - * Handles the user-defined `onLinkMouseOver` function. - */ - this.handleOnLinkMouseOverCb = (linkSource, linkTarget, evt) => { - const { onLinkMouseOver } = this.props; - if (onLinkMouseOver && typeof onLinkMouseOver === 'function') { - // Persist the SyntheticEvent for downstream handling by users. - evt.persist(); - onLinkMouseOver(clone(linkSource), clone(linkTarget), evt); - } - }; - /** - * Handles the user-defined `onNodeMouseOut` function. - */ - this.handleOnNodeMouseOutCb = (hierarchyPointNode, evt) => { - const { onNodeMouseOut } = this.props; - if (onNodeMouseOut && typeof onNodeMouseOut === 'function') { - // Persist the SyntheticEvent for downstream handling by users. - evt.persist(); - onNodeMouseOut(clone(hierarchyPointNode), evt); - } - }; - /** - * Handles the user-defined `onLinkMouseOut` function. - */ - this.handleOnLinkMouseOutCb = (linkSource, linkTarget, evt) => { - const { onLinkMouseOut } = this.props; - if (onLinkMouseOut && typeof onLinkMouseOut === 'function') { - // Persist the SyntheticEvent for downstream handling by users. - evt.persist(); - onLinkMouseOut(clone(linkSource), clone(linkTarget), evt); - } - }; - /** - * Takes a hierarchy point node and centers the node on the screen - * if the dimensions parameter is passed to `Tree`. - * - * This code is adapted from Rob Schmuecker's centerNode method. - * Link: http://bl.ocks.org/robschmuecker/7880033 - */ - this.centerNode = (hierarchyPointNode) => { - const { dimensions, orientation, zoom, centeringTransitionDuration } = this.props; - if (dimensions) { - const g = select(`.${this.gInstanceRef}`); - const svg = select(`.${this.svgInstanceRef}`); - const scale = this.state.d3.scale; - let x; - let y; - // if the orientation is horizontal, calculate the variables inverted (x->y, y->x) - if (orientation === 'horizontal') { - y = -hierarchyPointNode.x * scale + dimensions.height / 2; - x = -hierarchyPointNode.y * scale + dimensions.width / 2; - } - else { - // else, calculate the variables normally (x->x, y->y) - x = -hierarchyPointNode.x * scale + dimensions.width / 2; - y = -hierarchyPointNode.y * scale + dimensions.height / 2; - } - //@ts-ignore - g.transition() - .duration(centeringTransitionDuration) - .attr('transform', 'translate(' + x + ',' + y + ')scale(' + scale + ')'); - // Sets the viewport to the new center so that it does not jump back to original - // coordinates when dragged/zoomed - //@ts-ignore - svg.call(d3zoom().transform, zoomIdentity.translate(x, y).scale(zoom)); - } - }; - /** - * Determines which additional `className` prop should be passed to the node & returns it. - */ - this.getNodeClassName = (parent, nodeDatum) => { - const { rootNodeClassName, branchNodeClassName, leafNodeClassName } = this.props; - const hasParent = parent !== null && parent !== undefined; - if (hasParent) { - return nodeDatum.children ? branchNodeClassName : leafNodeClassName; - } - else { - return rootNodeClassName; - } - }; - } - static getDerivedStateFromProps(nextProps, prevState) { - let derivedState = null; - // Clone new data & assign internal properties if `data` object reference changed. - if (nextProps.data !== prevState.dataRef) { - derivedState = { - dataRef: nextProps.data, - data: Tree.assignInternalProperties(clone(nextProps.data)), - isInitialRenderForDataset: true, - }; - } - const d3 = Tree.calculateD3Geometry(nextProps); - if (!deepEqual(d3, prevState.d3)) { - derivedState = derivedState || {}; - derivedState.d3 = d3; - } - return derivedState; - } - componentDidMount() { - this.bindZoomListener(this.props); - this.setState({ isInitialRenderForDataset: false }); - } - componentDidUpdate(prevProps) { - if (this.props.data !== prevProps.data) { - // If last `render` was due to change in dataset -> mark the initial render as done. - this.setState({ isInitialRenderForDataset: false }); - } - if (!deepEqual(this.props.translate, prevProps.translate) || - !deepEqual(this.props.scaleExtent, prevProps.scaleExtent) || - this.props.zoomable !== prevProps.zoomable || - this.props.draggable !== prevProps.draggable || - this.props.zoom !== prevProps.zoom || - this.props.enableLegacyTransitions !== prevProps.enableLegacyTransitions) { - // If zoom-specific props change -> rebind listener with new values. - // Or: rebind zoom listeners to new DOM nodes in case legacy transitions were enabled/disabled. - this.bindZoomListener(this.props); - } - if (typeof this.props.onUpdate === 'function') { - this.props.onUpdate({ - node: this.internalState.targetNode ? clone(this.internalState.targetNode) : null, - zoom: this.state.d3.scale, - translate: this.state.d3.translate, - }); - } - // Reset the last target node after we've flushed it to `onUpdate`. - this.internalState.targetNode = null; - } - /** - * Collapses all tree nodes with a `depth` larger than `initialDepth`. - * - * @param {array} nodeSet Array of nodes generated by `generateTree` - * @param {number} initialDepth Maximum initial depth the tree should render - */ - setInitialTreeDepth(nodeSet, initialDepth) { - nodeSet.forEach(n => { - n.data.__rd3t.collapsed = n.depth >= initialDepth; - }); - } - /** - * bindZoomListener - If `props.zoomable`, binds a listener for - * "zoom" events to the SVG and sets scaleExtent to min/max - * specified in `props.scaleExtent`. - */ - bindZoomListener(props) { - const { zoomable, scaleExtent, translate, zoom, onUpdate, hasInteractiveNodes } = props; - const svg = select(`.${this.svgInstanceRef}`); - const g = select(`.${this.gInstanceRef}`); - // Sets initial offset, so that first pan and zoom does not jump back to default [0,0] coords. - // @ts-ignore - svg.call(d3zoom().transform, zoomIdentity.translate(translate.x, translate.y).scale(zoom)); - svg.call(d3zoom() - .scaleExtent(zoomable ? [scaleExtent.min, scaleExtent.max] : [zoom, zoom]) - // TODO: break this out into a separate zoom handler fn, rather than inlining it. - .filter((event) => { - if (hasInteractiveNodes) { - return (event.target.classList.contains(this.svgInstanceRef) || - event.target.classList.contains(this.gInstanceRef) || - event.shiftKey); - } - return true; - }) - .on('zoom', (event) => { - if (!this.props.draggable && - (event.sourceEvent.type === 'mousemove' || event.sourceEvent.type === 'touchmove')) { - return; - } - g.attr('transform', event.transform); - if (typeof onUpdate === 'function') { - // This callback is magically called not only on "zoom", but on "drag", as well, - // even though event.type == "zoom". - // Taking advantage of this and not writing a "drag" handler. - onUpdate({ - node: null, - zoom: event.transform.k, - translate: { x: event.transform.x, y: event.transform.y }, - }); - // TODO: remove this? Shouldn't be mutating state keys directly. - this.state.d3.scale = event.transform.k; - this.state.d3.translate = { - x: event.transform.x, - y: event.transform.y, - }; - } - })); - } - /** - * Assigns internal properties that are required for tree - * manipulation to each node in the `data` set and returns a new `data` array. - * - * @static - */ - static assignInternalProperties(data, currentDepth = 0) { - // Wrap the root node into an array for recursive transformations if it wasn't in one already. - const d = Array.isArray(data) ? data : [data]; - return d.map(n => { - const nodeDatum = n; - nodeDatum.__rd3t = { id: null, depth: null, collapsed: false }; - nodeDatum.__rd3t.id = uuidv4(); - // D3@v5 compat: manually assign `depth` to node.data so we don't have - // to hold full node+link sets in state. - // TODO: avoid this extra step by checking D3's node.depth directly. - nodeDatum.__rd3t.depth = currentDepth; - // If there are children, recursively assign properties to them too. - if (nodeDatum.children && nodeDatum.children.length > 0) { - nodeDatum.children = Tree.assignInternalProperties(nodeDatum.children, currentDepth + 1); - } - return nodeDatum; - }); - } - /** - * Recursively walks the nested `nodeSet` until a node matching `nodeId` is found. - */ - findNodesById(nodeId, nodeSet, hits) { - if (hits.length > 0) { - return hits; - } - hits = hits.concat(nodeSet.filter(node => node.__rd3t.id === nodeId)); - nodeSet.forEach(node => { - if (node.children && node.children.length > 0) { - hits = this.findNodesById(nodeId, node.children, hits); - } - }); - return hits; - } - /** - * Recursively walks the nested `nodeSet` until all nodes at `depth` have been found. - * - * @param {number} depth Target depth for which nodes should be returned - * @param {array} nodeSet Array of nested `node` objects - * @param {array} accumulator Accumulator for matches, passed between recursive calls - */ - findNodesAtDepth(depth, nodeSet, accumulator) { - accumulator = accumulator.concat(nodeSet.filter(node => node.__rd3t.depth === depth)); - nodeSet.forEach(node => { - if (node.children && node.children.length > 0) { - accumulator = this.findNodesAtDepth(depth, node.children, accumulator); - } - }); - return accumulator; - } - /** - * Recursively sets the internal `collapsed` property of - * the passed `TreeNodeDatum` and its children to `true`. - * - * @static - */ - static collapseNode(nodeDatum) { - nodeDatum.__rd3t.collapsed = true; - if (nodeDatum.children && nodeDatum.children.length > 0) { - nodeDatum.children.forEach(child => { - Tree.collapseNode(child); - }); - } - } - /** - * Sets the internal `collapsed` property of - * the passed `TreeNodeDatum` object to `false`. - * - * @static - */ - static expandNode(nodeDatum) { - nodeDatum.__rd3t.collapsed = false; - } - /** - * Collapses all nodes in `nodeSet` that are neighbors (same depth) of `targetNode`. - */ - collapseNeighborNodes(targetNode, nodeSet) { - const neighbors = this.findNodesAtDepth(targetNode.__rd3t.depth, nodeSet, []).filter(node => node.__rd3t.id !== targetNode.__rd3t.id); - neighbors.forEach(neighbor => Tree.collapseNode(neighbor)); - } - /** - * Generates tree elements (`nodes` and `links`) by - * grabbing the rootNode from `this.state.data[0]`. - * Restricts tree depth to `props.initialDepth` if defined and if this is - * the initial render of the tree. - */ - generateTree() { - const { initialDepth, depthFactor, separation, nodeSize, orientation } = this.props; - const { isInitialRenderForDataset } = this.state; - const tree = d3tree() - .nodeSize(orientation === 'horizontal' ? [nodeSize.y, nodeSize.x] : [nodeSize.x, nodeSize.y]) - .separation((a, b) => a.parent.data.__rd3t.id === b.parent.data.__rd3t.id - ? separation.siblings - : separation.nonSiblings); - const rootNode = tree(hierarchy(this.state.data[0], d => (d.__rd3t.collapsed ? null : d.children))); - let nodes = rootNode.descendants(); - const links = rootNode.links(); - // Configure nodes' `collapsed` property on first render if `initialDepth` is defined. - if (initialDepth !== undefined && isInitialRenderForDataset) { - this.setInitialTreeDepth(nodes, initialDepth); - } - if (depthFactor) { - nodes.forEach(node => { - node.y = node.depth * depthFactor; - }); - } - return { nodes, links }; - } - /** - * Set initial zoom and position. - * Also limit zoom level according to `scaleExtent` on initial display. This is necessary, - * because the first time we are setting it as an SVG property, instead of going - * through D3's scaling mechanism, which would have picked up both properties. - * - * @static - */ - static calculateD3Geometry(nextProps) { - let scale; - if (nextProps.zoom > nextProps.scaleExtent.max) { - scale = nextProps.scaleExtent.max; - } - else if (nextProps.zoom < nextProps.scaleExtent.min) { - scale = nextProps.scaleExtent.min; - } - else { - scale = nextProps.zoom; - } - return { - translate: nextProps.translate, - scale, - }; - } - render() { - const { nodes, links } = this.generateTree(); - const { renderCustomNodeElement, orientation, pathFunc, transitionDuration, nodeSize, depthFactor, initialDepth, separation, enableLegacyTransitions, svgClassName, pathClassFunc, } = this.props; - const { translate, scale } = this.state.d3; - const subscriptions = Object.assign(Object.assign(Object.assign({}, nodeSize), separation), { depthFactor, - initialDepth }); - return (React.createElement("div", { className: "rd3t-tree-container rd3t-grabbable" }, - React.createElement("style", null, globalCss), - React.createElement("svg", { className: `rd3t-svg ${this.svgInstanceRef} ${svgClassName}`, width: "100%", height: "100%" }, - React.createElement(TransitionGroupWrapper, { enableLegacyTransitions: enableLegacyTransitions, component: "g", className: `rd3t-g ${this.gInstanceRef}`, transform: `translate(${translate.x},${translate.y}) scale(${scale})` }, - links.map((linkData, i) => { - return (React.createElement(Link, { key: 'link-' + i, orientation: orientation, pathFunc: pathFunc, pathClassFunc: pathClassFunc, linkData: linkData, onClick: this.handleOnLinkClickCb, onMouseOver: this.handleOnLinkMouseOverCb, onMouseOut: this.handleOnLinkMouseOutCb, enableLegacyTransitions: enableLegacyTransitions, transitionDuration: transitionDuration })); - }), - nodes.map((hierarchyPointNode, i) => { - const { data, x, y, parent } = hierarchyPointNode; - return (React.createElement(Node, { key: 'node-' + i, data: data, position: { x, y }, hierarchyPointNode: hierarchyPointNode, parent: parent, nodeClassName: this.getNodeClassName(parent, data), renderCustomNodeElement: renderCustomNodeElement, nodeSize: nodeSize, orientation: orientation, enableLegacyTransitions: enableLegacyTransitions, transitionDuration: transitionDuration, onNodeToggle: this.handleNodeToggle, onNodeClick: this.handleOnNodeClickCb, onNodeMouseOver: this.handleOnNodeMouseOverCb, onNodeMouseOut: this.handleOnNodeMouseOutCb, subscriptions: subscriptions, centerNode: this.centerNode })); - }))))); - } -} -Tree.defaultProps = { - onNodeClick: undefined, - onNodeMouseOver: undefined, - onNodeMouseOut: undefined, - onLinkClick: undefined, - onLinkMouseOver: undefined, - onLinkMouseOut: undefined, - onUpdate: undefined, - orientation: 'horizontal', - translate: { x: 0, y: 0 }, - pathFunc: 'diagonal', - pathClassFunc: undefined, - transitionDuration: 500, - depthFactor: undefined, - collapsible: true, - initialDepth: undefined, - zoomable: true, - draggable: true, - zoom: 1, - scaleExtent: { min: 0.1, max: 1 }, - nodeSize: { x: 140, y: 140 }, - separation: { siblings: 1, nonSiblings: 2 }, - shouldCollapseNeighborNodes: false, - svgClassName: '', - rootNodeClassName: '', - branchNodeClassName: '', - leafNodeClassName: '', - renderCustomNodeElement: undefined, - enableLegacyTransitions: false, - hasInteractiveNodes: false, - dimensions: undefined, - centeringTransitionDuration: 800, -}; -export default Tree; diff --git a/lib/esm/Tree/types.js b/lib/esm/Tree/types.js deleted file mode 100644 index cb0ff5c3..00000000 --- a/lib/esm/Tree/types.js +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/lib/esm/globalCss.js b/lib/esm/globalCss.js deleted file mode 100644 index 134728a2..00000000 --- a/lib/esm/globalCss.js +++ /dev/null @@ -1,59 +0,0 @@ -// Importing CSS files globally (e.g. `import "./styles.css"`) can cause resolution issues with certain -// libraries/frameworks. -// Example: Next.js (https://github.com/vercel/next.js/blob/master/errors/css-npm.md) -// -// Since rd3t's CSS is bare bones to begin with, we provide all required styles as a template string, -// which can be imported like any other TS/JS module and inlined into a `` tag. -export default ` -/* Tree */ -.rd3t-tree-container { - width: 100%; - height: 100%; -} - -.rd3t-grabbable { - cursor: move; /* fallback if grab cursor is unsupported */ - cursor: grab; - cursor: -moz-grab; - cursor: -webkit-grab; -} -.rd3t-grabbable:active { - cursor: grabbing; - cursor: -moz-grabbing; - cursor: -webkit-grabbing; -} - -/* Node */ -.rd3t-node { - cursor: pointer; - fill: #777; - stroke: #000; - stroke-width: 2; -} - -.rd3t-leaf-node { - cursor: pointer; - fill: transparent; - stroke: #000; - stroke-width: 1; -} - -.rd3t-label__title { - fill: #000; - stroke: none; - font-weight: bolder; -} - -.rd3t-label__attributes { - fill: #777; - stroke: none; - font-weight: bolder; - font-size: smaller; -} - -/* Link */ -.rd3t-link { - fill: none; - stroke: #000; -} -`; diff --git a/lib/esm/index.js b/lib/esm/index.js deleted file mode 100644 index e8f3a713..00000000 --- a/lib/esm/index.js +++ /dev/null @@ -1,2 +0,0 @@ -import Tree from './Tree/index.js'; -export default Tree; diff --git a/lib/esm/types/common.js b/lib/esm/types/common.js deleted file mode 100644 index cb0ff5c3..00000000 --- a/lib/esm/types/common.js +++ /dev/null @@ -1 +0,0 @@ -export {};