This commit is contained in:
Nathan Bierema 2022-12-28 16:43:02 -05:00
parent 046c69c5f7
commit 166ee67057
3 changed files with 93 additions and 118 deletions

View File

@ -1,4 +1,4 @@
import d3, { ZoomEvent, Primitive } from 'd3'; import d3, { ZoomEvent } from 'd3';
import { isEmpty } from 'ramda'; import { isEmpty } from 'ramda';
import { map2tree } from 'map2tree'; import { map2tree } from 'map2tree';
import deepmerge from 'deepmerge'; import deepmerge from 'deepmerge';
@ -9,6 +9,7 @@ import {
getNodeGroupByDepthCount, getNodeGroupByDepthCount,
} from './utils'; } from './utils';
import { tooltip } from 'd3tooltip'; import { tooltip } from 'd3tooltip';
import type { StyleValue } from 'd3tooltip';
// TODO Can we remove InputOptions? // TODO Can we remove InputOptions?
export interface InputOptions { export interface InputOptions {
@ -20,7 +21,7 @@ export interface InputOptions {
rootKeyName: string; rootKeyName: string;
pushMethod: 'push' | 'unshift'; pushMethod: 'push' | 'unshift';
id: string; id: string;
chartStyles: { [key: string]: Primitive }; chartStyles: { [key: string]: StyleValue };
nodeStyleOptions: { nodeStyleOptions: {
colors: { colors: {
default: string; default: string;
@ -35,7 +36,7 @@ export interface InputOptions {
hover: string; hover: string;
}; };
}; };
linkStyles: { [key: string]: Primitive }; linkStyles: { [key: string]: StyleValue };
size: number; size: number;
aspectRatio: number; aspectRatio: number;
initialZoom: number; initialZoom: number;
@ -59,7 +60,7 @@ export interface InputOptions {
left: number; left: number;
top: number; top: number;
}; };
style?: { [key: string]: Primitive } | undefined; style?: { [key: string]: StyleValue } | undefined;
indentationSize?: number; indentationSize?: number;
}; };
} }
@ -73,7 +74,7 @@ interface Options {
rootKeyName: string; rootKeyName: string;
pushMethod: 'push' | 'unshift'; pushMethod: 'push' | 'unshift';
id: string; id: string;
chartStyles: { [key: string]: Primitive }; chartStyles: { [key: string]: StyleValue };
nodeStyleOptions: { nodeStyleOptions: {
colors: { colors: {
default: string; default: string;
@ -88,7 +89,7 @@ interface Options {
hover: string; hover: string;
}; };
}; };
linkStyles: { [key: string]: Primitive }; linkStyles: { [key: string]: StyleValue };
size: number; size: number;
aspectRatio: number; aspectRatio: number;
initialZoom: number; initialZoom: number;
@ -112,7 +113,7 @@ interface Options {
left: number; left: number;
top: number; top: number;
}; };
style: { [key: string]: Primitive } | undefined; style: { [key: string]: StyleValue } | undefined;
indentationSize?: number; indentationSize?: number;
}; };
} }
@ -241,12 +242,11 @@ export default function (
svgElement = svgElement.style('cursor', '-webkit-grab'); svgElement = svgElement.style('cursor', '-webkit-grab');
const vis = root for (const [key, value] of Object.entries(chartStyles)) {
.append('svg') svgElement = svgElement.style(key, value);
.attr(attr) }
.style({ cursor: '-webkit-grab', ...chartStyles } as unknown as {
[key: string]: Primitive; const vis = svgElement
})
.call( .call(
zoom.on('zoom', () => { zoom.on('zoom', () => {
const { translate, scale } = d3.event as ZoomEvent; const { translate, scale } = d3.event as ZoomEvent;
@ -257,11 +257,12 @@ export default function (
}) })
) )
.append('g') .append('g')
.attr({ .attr(
transform: `translate(${margin.left + nodeStyleOptions.radius}, ${ 'transform',
`translate(${margin.left + nodeStyleOptions.radius}, ${
margin.top margin.top
}) scale(${initialZoom})`, }) scale(${initialZoom})`
}); );
let layout = d3.layout.tree().size([width, height]); let layout = d3.layout.tree().size([width, height]);
let data: NodeWithId; let data: NodeWithId;
@ -384,9 +385,8 @@ export default function (
const nodeEnter = node const nodeEnter = node
.enter() .enter()
.append('g') .append('g')
.attr({ .attr('class', 'node')
class: 'node', .attr('transform', (d) => {
transform: (d) => {
const position = findParentNodePosition( const position = findParentNodePosition(
nodePositionsById, nodePositionsById,
d.id, d.id,
@ -396,12 +396,9 @@ export default function (
(position && previousNodePositionsById[position.id]) || (position && previousNodePositionsById[position.id]) ||
previousNodePositionsById.root; previousNodePositionsById.root;
return `translate(${previousPosition.y!},${previousPosition.x!})`; return `translate(${previousPosition.y!},${previousPosition.x!})`;
},
})
.style({
fill: textStyleOptions.colors.default,
cursor: 'pointer',
}) })
.style('fill', textStyleOptions.colors.default)
.style('cursor', 'pointer')
.on('mouseover', function mouseover(this: EventTarget) { .on('mouseover', function mouseover(this: EventTarget) {
d3.select(this).style({ d3.select(this).style({
fill: textStyleOptions.colors.hover, fill: textStyleOptions.colors.hover,
@ -426,10 +423,8 @@ export default function (
const nodeEnterInnerGroup = nodeEnter.append('g'); const nodeEnterInnerGroup = nodeEnter.append('g');
nodeEnterInnerGroup nodeEnterInnerGroup
.append('circle') .append('circle')
.attr({ .attr('class', 'nodeCircle')
class: 'nodeCircle', .attr('r', 0)
r: 0,
})
.on('click', (clickedNode) => { .on('click', (clickedNode) => {
if ((d3.event as Event).defaultPrevented) return; if ((d3.event as Event).defaultPrevented) return;
toggleChildren(clickedNode); toggleChildren(clickedNode);
@ -438,15 +433,11 @@ export default function (
nodeEnterInnerGroup nodeEnterInnerGroup
.append('text') .append('text')
.attr({ .attr('class', 'nodeText')
class: 'nodeText', .attr('text-anchor', 'middle')
'text-anchor': 'middle', .attr('transform', 'translate(0,0)')
transform: 'translate(0,0)', .attr('dy', '.35em')
dy: '.35em', .style('fill-opacity', 0)
})
.style({
'fill-opacity': 0,
})
.text((d) => d.name) .text((d) => d.name)
.on('click', onClickText); .on('click', onClickText);
@ -454,24 +445,23 @@ export default function (
node.select('text').text((d) => d.name); node.select('text').text((d) => d.name);
// change the circle fill depending on whether it has children and is collapsed // change the circle fill depending on whether it has children and is collapsed
node.select('circle').style({ node
stroke: 'black', .select('circle')
'stroke-width': '1.5px', .style('stroke', 'black')
fill: (d) => .style('stroke-width', '1.5px')
.style('fill', (d) =>
d._children d._children
? nodeStyleOptions.colors.collapsed ? nodeStyleOptions.colors.collapsed
: d.children : d.children
? nodeStyleOptions.colors.parent ? nodeStyleOptions.colors.parent
: nodeStyleOptions.colors.default, : nodeStyleOptions.colors.default
}); );
// transition nodes to their new position // transition nodes to their new position
const nodeUpdate = node const nodeUpdate = node
.transition() .transition()
.duration(transitionDuration) .duration(transitionDuration)
.attr({ .attr('transform', (d) => `translate(${d.y!},${d.x!})`);
transform: (d) => `translate(${d.y!},${d.x!})`,
});
// ensure circle radius is correct // ensure circle radius is correct
nodeUpdate.select('circle').attr('r', nodeStyleOptions.radius); nodeUpdate.select('circle').attr('r', nodeStyleOptions.radius);
@ -480,13 +470,11 @@ export default function (
nodeUpdate nodeUpdate
.select('text') .select('text')
.style('fill-opacity', 1) .style('fill-opacity', 1)
.attr({ .attr('transform', function transform(this: SVGGraphicsElement, d) {
transform: function transform(this: SVGGraphicsElement, d) {
const x = const x =
(d.children || d._children ? -1 : 1) * (d.children || d._children ? -1 : 1) *
(this.getBBox().width / 2 + nodeStyleOptions.radius + 5); (this.getBBox().width / 2 + nodeStyleOptions.radius + 5);
return `translate(${x},0)`; return `translate(${x},0)`;
},
}); });
// blink updated nodes // blink updated nodes
@ -509,8 +497,7 @@ export default function (
.exit() .exit()
.transition() .transition()
.duration(transitionDuration) .duration(transitionDuration)
.attr({ .attr('transform', (d) => {
transform: (d) => {
const position = findParentNodePosition( const position = findParentNodePosition(
previousNodePositionsById, previousNodePositionsById,
d.id, d.id,
@ -520,7 +507,6 @@ export default function (
(position && nodePositionsById[position.id]) || (position && nodePositionsById[position.id]) ||
nodePositionsById.root; nodePositionsById.root;
return `translate(${futurePosition.y!},${futurePosition.x!})`; return `translate(${futurePosition.y!},${futurePosition.x!})`;
},
}) })
.remove(); .remove();
@ -537,9 +523,8 @@ export default function (
link link
.enter() .enter()
.insert('path', 'g') .insert('path', 'g')
.attr({ .attr('class', 'link')
class: 'link', .attr('d', (d) => {
d: (d) => {
const position = findParentNodePosition( const position = findParentNodePosition(
nodePositionsById, nodePositionsById,
(d.target as NodeWithId).id, (d.target as NodeWithId).id,
@ -552,25 +537,18 @@ export default function (
source: previousPosition, source: previousPosition,
target: previousPosition, target: previousPosition,
} as d3.svg.diagonal.Link<NodePosition>); } as d3.svg.diagonal.Link<NodePosition>);
},
}) })
.style(linkStyles); .style(linkStyles);
// transition links to their new position // transition links to their new position
link link.transition().duration(transitionDuration).attr('d', diagonal);
.transition()
.duration(transitionDuration)
.attr({
d: diagonal as unknown as Primitive,
});
// transition exiting nodes to the parent's new position // transition exiting nodes to the parent's new position
link link
.exit() .exit()
.transition() .transition()
.duration(transitionDuration) .duration(transitionDuration)
.attr({ .attr('d', (d) => {
d: (d) => {
const position = findParentNodePosition( const position = findParentNodePosition(
previousNodePositionsById, previousNodePositionsById,
(d.target as NodeWithId).id, (d.target as NodeWithId).id,
@ -583,7 +561,6 @@ export default function (
source: futurePosition, source: futurePosition,
target: futurePosition, target: futurePosition,
}); });
},
}) })
.remove(); .remove();
@ -595,5 +572,3 @@ export default function (
} }
}; };
} }
export { Primitive };

View File

@ -1,2 +1,2 @@
export { tree } from './charts'; export { tree } from './charts';
export type { InputOptions, NodeWithId, Primitive } from './charts'; export type { InputOptions, NodeWithId } from './charts';

View File

@ -24,7 +24,7 @@ const defaultOptions: Options<ContainerElement, unknown, BaseType, unknown> = {
root: undefined, root: undefined,
}; };
type StyleValue = string | number | boolean; export type StyleValue = string | number | boolean;
interface Tip< interface Tip<
GElement extends ContainerElement, GElement extends ContainerElement,
@ -62,7 +62,7 @@ export function tooltip<
index?: number, index?: number,
outerIndex?: number outerIndex?: number
) => string = () => ''; ) => string = () => '';
let styles = {}; let styles: { [key: string]: StyleValue } = {};
let el: Selection<HTMLDivElement, Datum, BaseType, PDatum>; let el: Selection<HTMLDivElement, Datum, BaseType, PDatum>;
const anchor: Selection<GElement, Datum, BaseType, PDatum> = const anchor: Selection<GElement, Datum, BaseType, PDatum> =
@ -85,7 +85,7 @@ export function tooltip<
.style('top', `${y}px`); .style('top', `${y}px`);
for (const [key, value] of Object.entries(styles)) { for (const [key, value] of Object.entries(styles)) {
el = el.style(key, value as StyleValue); el = el.style(key, value);
} }
el = el.html(() => text(node)); el = el.html(() => text(node));