diff --git a/packages/d3-state-visualizer/src/charts/tree/tree.ts b/packages/d3-state-visualizer/src/charts/tree/tree.ts index ed0eeb44..30a66ee0 100644 --- a/packages/d3-state-visualizer/src/charts/tree/tree.ts +++ b/packages/d3-state-visualizer/src/charts/tree/tree.ts @@ -1,4 +1,5 @@ -import d3, { D3ZoomEvent } from 'd3'; +import * as d3 from 'd3'; +import type { D3ZoomEvent, HierarchyPointLink, HierarchyPointNode } from 'd3'; import { isEmpty } from 'ramda'; import { map2tree } from 'map2tree'; import deepmerge from 'deepmerge'; @@ -16,7 +17,7 @@ export interface InputOptions { // eslint-disable-next-line @typescript-eslint/ban-types state?: {} | null; // eslint-disable-next-line @typescript-eslint/ban-types - tree?: NodeWithId | {}; + tree?: Node | {}; rootKeyName: string; pushMethod: 'push' | 'unshift'; @@ -51,7 +52,7 @@ export interface InputOptions { widthBetweenNodesCoeff: number; transitionDuration: number; blinkDuration: number; - onClickText: (datum: NodeWithId) => void; + onClickText: (datum: Node) => void; tooltipOptions: { disabled?: boolean; left?: number | undefined; @@ -69,7 +70,7 @@ interface Options { // eslint-disable-next-line @typescript-eslint/ban-types state?: {} | null; // eslint-disable-next-line @typescript-eslint/ban-types - tree?: NodeWithId | {}; + tree?: Node | {}; rootKeyName: string; pushMethod: 'push' | 'unshift'; @@ -172,19 +173,37 @@ const defaultOptions: Options = { }, }; -export interface NodeWithId { +export interface Node { name: string; - children?: NodeWithId[] | null; - _children?: NodeWithId[] | null; + children?: Node[] | null; + value?: unknown; +} + +export interface InternalNode { + name: string; + children?: this[] | null; value?: unknown; id: string; - - parent?: NodeWithId; - depth?: number; - x?: number; - y?: number; } +export interface HierarchyPointNodeWithPrivateChildren + extends HierarchyPointNode { + _children?: this[] | undefined; +} + +// export interface NodeWithId { +// name: string; +// children?: NodeWithId[] | null; +// _children?: NodeWithId[] | null; +// value?: unknown; +// id: string; +// +// parent?: NodeWithId; +// depth?: number; +// x?: number; +// y?: number; +// } + interface NodePosition { parentId: string | null | undefined; id: string; @@ -263,17 +282,17 @@ export default function ( }) scale(${initialZoom})` ); - let layout = d3.layout.tree().size([width, height]); - let data: NodeWithId; + // let layout = d3.tree().size([width, height]); + let data: InternalNode; - if (isSorted) { - layout.sort((a, b) => - (b as NodeWithId).name.toLowerCase() < - (a as NodeWithId).name.toLowerCase() - ? 1 - : -1 - ); - } + // if (isSorted) { + // layout.sort((a, b) => + // (b as NodeWithId).name.toLowerCase() < + // (a as NodeWithId).name.toLowerCase() + // ? 1 + // : -1 + // ); + // } // previousNodePositionsById stores node x and y // as well as hierarchy (id / parentId); @@ -309,18 +328,17 @@ export default function ( return function renderChart(nextState = tree || state) { data = !tree - ? // eslint-disable-next-line @typescript-eslint/ban-types - (map2tree(nextState as {}, { + ? (map2tree(nextState, { key: rootKeyName, pushMethod, - }) as NodeWithId) - : (nextState as NodeWithId); + }) as InternalNode) + : (nextState as InternalNode); if (isEmpty(data) || !data.name) { data = { name: 'error', message: 'Please provide a state map or a tree structure', - } as unknown as NodeWithId; + } as unknown as InternalNode; } let nodeIndex = 0; @@ -354,22 +372,32 @@ export default function ( // set tree dimensions and spacing between branches and nodes const maxNodeCountByLevel = Math.max(...getNodeGroupByDepthCount(data)); - layout = layout.size([ - maxNodeCountByLevel * 25 * heightBetweenNodesCoeff, - width, - ]); + const layout = d3 + .tree() + .size([maxNodeCountByLevel * 25 * heightBetweenNodesCoeff, width]); - const nodes = layout.nodes(data as d3.layout.tree.Node) as NodeWithId[]; - const links = layout.links(nodes as d3.layout.tree.Node[]); + const rootNode = d3.hierarchy(data); + if (isSorted) { + rootNode.sort((a, b) => + b.data.name.toLowerCase() < a.data.name.toLowerCase() ? 1 : -1 + ); + } - nodes.forEach( + const rootPointNode = layout( + rootNode + ) as HierarchyPointNodeWithPrivateChildren; + const links = rootPointNode.links(); + + rootPointNode.each( (node) => - (node.y = node.depth! * (maxLabelLength * 7 * widthBetweenNodesCoeff)) + (node.y = node.depth * (maxLabelLength * 7 * widthBetweenNodesCoeff)) ); + const nodes = rootPointNode.descendants(); + const nodePositions = nodes.map((n) => ({ - parentId: n.parent && n.parent.id, - id: n.id, + parentId: n.parent && n.parent.data.id, + id: n.data.id, x: n.x, y: n.y, })); @@ -378,9 +406,15 @@ export default function ( // process the node selection const node = vis - .selectAll('g.node') - .property('__oldData__', (d: NodeWithId) => d) - .data(nodes, (d) => d.id || (d.id = ++nodeIndex as unknown as string)); + .selectAll< + SVGGElement, + HierarchyPointNodeWithPrivateChildren + >('g.node') + .property('__oldData__', (d) => d) + .data( + nodes, + (d) => d.data.id || (d.data.id = ++nodeIndex as unknown as string) + ); const nodeEnter = node .enter() .append('g') @@ -388,7 +422,7 @@ export default function ( .attr('transform', (d) => { const position = findParentNodePosition( nodePositionsById, - d.id, + d.data.id, (n) => !!previousNodePositionsById[n.id] ); const previousPosition = @@ -411,9 +445,9 @@ export default function ( if (!tooltipOptions.disabled) { nodeEnter.call( - tooltip(d3, 'tooltip', { ...tooltipOptions, root }) + tooltip('tooltip', { ...tooltipOptions, root }) .text((d, i) => getTooltipString(d, i, tooltipOptions)) - .style(tooltipOptions.style) + .styles(tooltipOptions.style) ); } @@ -437,11 +471,11 @@ export default function ( .attr('transform', 'translate(0,0)') .attr('dy', '.35em') .style('fill-opacity', 0) - .text((d) => d.name) + .text((d) => d.data.name) .on('click', onClickText); // update the text to reflect whether node has children or not - node.select('text').text((d) => d.name); + node.select('text').text((d) => d.data.name); // change the circle fill depending on whether it has children and is collapsed node @@ -515,8 +549,8 @@ export default function ( // update the links const link = vis - .selectAll('path.link') - .data(links, (d) => (d.target as NodeWithId).id); + .selectAll>('path.link') + .data(links, (d) => d.target.data.id); // enter any new links at the parent's previous position link @@ -526,7 +560,7 @@ export default function ( .attr('d', (d) => { const position = findParentNodePosition( nodePositionsById, - (d.target as NodeWithId).id, + d.target.data.id, (n) => !!previousNodePositionsById[n.id] ); const previousPosition = @@ -535,9 +569,12 @@ export default function ( return diagonal({ source: previousPosition, target: previousPosition, - } as d3.svg.diagonal.Link); - }) - .style(linkStyles); + }); + }); + + for (const [key, value] of Object.entries(linkStyles)) { + link.style(key, value); + } // transition links to their new position link.transition().duration(transitionDuration).attr('d', diagonal); @@ -550,7 +587,7 @@ export default function ( .attr('d', (d) => { const position = findParentNodePosition( previousNodePositionsById, - (d.target as NodeWithId).id, + d.target.data.id, (n) => !!nodePositionsById[n.id] ); const futurePosition = diff --git a/packages/d3-state-visualizer/src/charts/tree/utils.ts b/packages/d3-state-visualizer/src/charts/tree/utils.ts index 10a87a3c..6b32b603 100644 --- a/packages/d3-state-visualizer/src/charts/tree/utils.ts +++ b/packages/d3-state-visualizer/src/charts/tree/utils.ts @@ -1,38 +1,47 @@ import { is, join, pipe, replace } from 'ramda'; import sortAndSerialize from './sortAndSerialize'; -import { NodeWithId } from './tree'; +import type { + HierarchyPointNodeWithPrivateChildren, + InternalNode, +} from './tree'; -export function collapseChildren(node: NodeWithId) { +export function collapseChildren( + node: HierarchyPointNodeWithPrivateChildren +) { if (node.children) { node._children = node.children; node._children.forEach(collapseChildren); - node.children = null; + node.children = undefined; } } -export function expandChildren(node: NodeWithId) { +export function expandChildren( + node: HierarchyPointNodeWithPrivateChildren +) { if (node._children) { node.children = node._children; node.children.forEach(expandChildren); - node._children = null; + node._children = undefined; } } -export function toggleChildren(node: NodeWithId) { +export function toggleChildren( + node: HierarchyPointNodeWithPrivateChildren +) { if (node.children) { node._children = node.children; - node.children = null; + node.children = undefined; } else if (node._children) { node.children = node._children; - node._children = null; + node._children = undefined; } return node; } export function visit( - parent: NodeWithId, - visitFn: (parent: NodeWithId) => void, - childrenFn: (parent: NodeWithId) => NodeWithId[] | null | undefined + parent: InternalNode, + visitFn: (parent: InternalNode) => void, + childrenFn: (parent: InternalNode) => InternalNode[] | null | undefined ) { if (!parent) { return; @@ -50,10 +59,10 @@ export function visit( } } -export function getNodeGroupByDepthCount(rootNode: NodeWithId) { +export function getNodeGroupByDepthCount(rootNode: InternalNode) { const nodeGroupByDepthCount = [1]; - const traverseFrom = function traverseFrom(node: NodeWithId, depth = 0) { + const traverseFrom = function traverseFrom(node: InternalNode, depth = 0) { if (!node.children || node.children.length === 0) { return 0; } diff --git a/packages/d3tooltip/src/index.ts b/packages/d3tooltip/src/index.ts index 70b3c89f..9c5c9fe5 100644 --- a/packages/d3tooltip/src/index.ts +++ b/packages/d3tooltip/src/index.ts @@ -1,4 +1,5 @@ -import d3Package, { BaseType, ContainerElement, Selection } from 'd3'; +import * as d3 from 'd3'; +import type { BaseType, ContainerElement, Selection } from 'd3'; import { is } from 'ramda'; import functor from './utils/functor'; @@ -48,7 +49,6 @@ export function tooltip< PElement extends BaseType, PDatum >( - d3: typeof d3Package, className = 'tooltip', options: Partial> = {} ): Tip {