mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-02-07 15:10:45 +03:00
feat(d3-state-visualizer): convert to TypeScript (#640)
* feat(d3-state-visualizer): convert to TypeScript * dep * Odd
This commit is contained in:
parent
3b580dad4c
commit
0c78a5a9a7
|
@ -1,7 +1,4 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-export-default-from"
|
||||
]
|
||||
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||
}
|
||||
|
|
3
packages/d3-state-visualizer/.eslintignore
Normal file
3
packages/d3-state-visualizer/.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
examples
|
||||
lib
|
||||
dist
|
21
packages/d3-state-visualizer/.eslintrc.js
Normal file
21
packages/d3-state-visualizer/.eslintrc.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
module.exports = {
|
||||
extends: '../../.eslintrc',
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts'],
|
||||
extends: '../../eslintrc.ts.base.json',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['webpack.config.umd.ts'],
|
||||
extends: '../../eslintrc.ts.base.json',
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.webpack.json'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -2,24 +2,6 @@
|
|||
"name": "d3-state-visualizer",
|
||||
"version": "1.3.4",
|
||||
"description": "Visualize your app state with a range of reusable charts",
|
||||
"main": "lib/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "babel src --out-dir lib",
|
||||
"build:umd": "webpack src/index.js -o dist/d3-state-visualizer.js --config webpack.config.development.js",
|
||||
"build:umd:min": "webpack src/index.js -o dist/d3-state-visualizer.min.js --config webpack.config.production.js",
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm run build:umd && npm run build:umd:min"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/reduxjs/redux-devtools.git"
|
||||
},
|
||||
"keywords": [
|
||||
"d3",
|
||||
"state",
|
||||
|
@ -27,27 +9,46 @@
|
|||
"tree",
|
||||
"visualization"
|
||||
],
|
||||
"author": "romseguy",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/reduxjs/redux-devtools/tree/master/packages/d3-state-visualizer",
|
||||
"bugs": {
|
||||
"url": "https://github.com/reduxjs/redux-devtools/issues"
|
||||
},
|
||||
"homepage": "https://github.com/reduxjs/redux-devtools",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.10.5",
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.10.4",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"webpack": "^4.44.1"
|
||||
"license": "MIT",
|
||||
"author": "romseguy",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/reduxjs/redux-devtools.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:types && npm run build:js && npm run build:umd && npm run build:umd:min",
|
||||
"build:types": "tsc --emitDeclarationOnly",
|
||||
"build:js": "babel src --out-dir lib --extensions \".ts\" --source-maps inline",
|
||||
"build:umd": "webpack --env.production --progress --config webpack.config.umd.ts",
|
||||
"build:umd:min": "webpack --env.production --progress --config webpack.config.umd.ts",
|
||||
"clean": "rimraf lib dist",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"type-check": "tsc --noEmit",
|
||||
"type-check:watch": "npm run type-check -- --watch",
|
||||
"preversion": "npm run type-check && npm run lint",
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/d3": "^3.5.43",
|
||||
"d3": "^3.5.17",
|
||||
"d3tooltip": "^1.2.3",
|
||||
"deepmerge": "^4.2.2",
|
||||
"map2tree": "^1.4.2",
|
||||
"ramda": "^0.27.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ramda": "^0.27.17"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export tree from './tree/tree';
|
1
packages/d3-state-visualizer/src/charts/index.ts
Normal file
1
packages/d3-state-visualizer/src/charts/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as tree } from './tree/tree';
|
|
@ -1,4 +1,4 @@
|
|||
function sortObject(obj, strict) {
|
||||
function sortObject(obj: unknown, strict?: boolean) {
|
||||
if (obj instanceof Array) {
|
||||
let ary;
|
||||
if (strict) {
|
||||
|
@ -10,16 +10,16 @@ function sortObject(obj, strict) {
|
|||
}
|
||||
|
||||
if (obj && typeof obj === 'object') {
|
||||
const tObj = {};
|
||||
const tObj: { [key: string]: unknown } = {};
|
||||
Object.keys(obj)
|
||||
.sort()
|
||||
.forEach((key) => (tObj[key] = sortObject(obj[key])));
|
||||
.forEach((key) => (tObj[key] = sortObject(obj[key as keyof typeof obj])));
|
||||
return tObj;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export default function sortAndSerialize(obj) {
|
||||
export default function sortAndSerialize(obj: unknown) {
|
||||
return JSON.stringify(sortObject(obj, true), undefined, 2);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import d3 from 'd3';
|
||||
import d3, { ZoomEvent, Primitive } from 'd3';
|
||||
import { isEmpty } from 'ramda';
|
||||
import map2tree from 'map2tree';
|
||||
import deepmerge from 'deepmerge';
|
||||
|
@ -10,7 +10,63 @@ import {
|
|||
} from './utils';
|
||||
import d3tooltip from 'd3tooltip';
|
||||
|
||||
const defaultOptions = {
|
||||
interface Options {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
state?: {};
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
tree?: NodeWithId | {};
|
||||
|
||||
rootKeyName: string;
|
||||
pushMethod: 'push' | 'unshift';
|
||||
id: string;
|
||||
style: {
|
||||
node: {
|
||||
colors: {
|
||||
default: string;
|
||||
collapsed: string;
|
||||
parent: string;
|
||||
};
|
||||
radius: number;
|
||||
};
|
||||
text: {
|
||||
colors: {
|
||||
default: string;
|
||||
hover: string;
|
||||
};
|
||||
};
|
||||
link: {
|
||||
stroke: string;
|
||||
fill: string;
|
||||
};
|
||||
};
|
||||
size: number;
|
||||
aspectRatio: number;
|
||||
initialZoom: number;
|
||||
margin: {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
};
|
||||
isSorted: boolean;
|
||||
heightBetweenNodesCoeff: number;
|
||||
widthBetweenNodesCoeff: number;
|
||||
transitionDuration: number;
|
||||
blinkDuration: number;
|
||||
onClickText: () => void;
|
||||
tooltipOptions: {
|
||||
disabled: boolean;
|
||||
left: number | undefined;
|
||||
top: number | undefined;
|
||||
offset: {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
style: { [key: string]: Primitive } | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultOptions: Options = {
|
||||
state: undefined,
|
||||
rootKeyName: 'state',
|
||||
pushMethod: 'push',
|
||||
|
@ -50,11 +106,13 @@ const defaultOptions = {
|
|||
widthBetweenNodesCoeff: 1,
|
||||
transitionDuration: 750,
|
||||
blinkDuration: 100,
|
||||
onClickText: () => {},
|
||||
onClickText: () => {
|
||||
// noop
|
||||
},
|
||||
tooltipOptions: {
|
||||
disabled: false,
|
||||
left: undefined,
|
||||
right: undefined,
|
||||
top: undefined,
|
||||
offset: {
|
||||
left: 0,
|
||||
top: 0,
|
||||
|
@ -63,7 +121,27 @@ const defaultOptions = {
|
|||
},
|
||||
};
|
||||
|
||||
export default function (DOMNode, options = {}) {
|
||||
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;
|
||||
x: number | undefined;
|
||||
y: number | undefined;
|
||||
}
|
||||
|
||||
export default function (DOMNode: HTMLElement, options: Partial<Options> = {}) {
|
||||
const {
|
||||
id,
|
||||
style,
|
||||
|
@ -89,16 +167,19 @@ export default function (DOMNode, options = {}) {
|
|||
const fullWidth = size;
|
||||
const fullHeight = size * aspectRatio;
|
||||
|
||||
const attr = {
|
||||
const attr: { [key: string]: Primitive } = {
|
||||
id,
|
||||
preserveAspectRatio: 'xMinYMin slice',
|
||||
};
|
||||
|
||||
if (!style.width) {
|
||||
if (!((style as unknown) as { [key: string]: Primitive }).width) {
|
||||
attr.width = fullWidth;
|
||||
}
|
||||
|
||||
if (!style.width || !style.height) {
|
||||
if (
|
||||
!((style as unknown) as { [key: string]: Primitive }).width ||
|
||||
!((style as unknown) as { [key: string]: Primitive }).height
|
||||
) {
|
||||
attr.viewBox = `0 0 ${fullWidth} ${fullHeight}`;
|
||||
}
|
||||
|
||||
|
@ -107,11 +188,16 @@ export default function (DOMNode, options = {}) {
|
|||
const vis = root
|
||||
.append('svg')
|
||||
.attr(attr)
|
||||
.style({ cursor: '-webkit-grab', ...style })
|
||||
.style(({ cursor: '-webkit-grab', ...style } as unknown) as {
|
||||
[key: string]: Primitive;
|
||||
})
|
||||
.call(
|
||||
zoom.on('zoom', () => {
|
||||
const { translate, scale } = d3.event;
|
||||
vis.attr('transform', `translate(${translate})scale(${scale})`);
|
||||
const { translate, scale } = d3.event as ZoomEvent;
|
||||
vis.attr(
|
||||
'transform',
|
||||
`translate(${translate.toString()})scale(${scale})`
|
||||
);
|
||||
})
|
||||
)
|
||||
.append('g')
|
||||
|
@ -122,18 +208,21 @@ export default function (DOMNode, options = {}) {
|
|||
});
|
||||
|
||||
let layout = d3.layout.tree().size([width, height]);
|
||||
let data;
|
||||
let data: NodeWithId;
|
||||
|
||||
if (isSorted) {
|
||||
layout.sort((a, b) =>
|
||||
b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1
|
||||
(b as NodeWithId).name.toLowerCase() <
|
||||
(a as NodeWithId).name.toLowerCase()
|
||||
? 1
|
||||
: -1
|
||||
);
|
||||
}
|
||||
|
||||
// previousNodePositionsById stores node x and y
|
||||
// as well as hierarchy (id / parentId);
|
||||
// helps animating transitions
|
||||
let previousNodePositionsById = {
|
||||
let previousNodePositionsById: { [nodeId: string]: NodePosition } = {
|
||||
root: {
|
||||
id: 'root',
|
||||
parentId: null,
|
||||
|
@ -145,10 +234,14 @@ export default function (DOMNode, options = {}) {
|
|||
// traverses a map with node positions by going through the chain
|
||||
// of parent ids; once a parent that matches the given filter is found,
|
||||
// the parent position gets returned
|
||||
function findParentNodePosition(nodePositionsById, nodeId, filter) {
|
||||
function findParentNodePosition(
|
||||
nodePositionsById: { [nodeId: string]: NodePosition },
|
||||
nodeId: string,
|
||||
filter: (nodePosition: NodePosition) => boolean
|
||||
) {
|
||||
let currentPosition = nodePositionsById[nodeId];
|
||||
while (currentPosition) {
|
||||
currentPosition = nodePositionsById[currentPosition.parentId];
|
||||
currentPosition = nodePositionsById[currentPosition.parentId!];
|
||||
if (!currentPosition) {
|
||||
return null;
|
||||
}
|
||||
|
@ -160,14 +253,18 @@ export default function (DOMNode, options = {}) {
|
|||
|
||||
return function renderChart(nextState = tree || state) {
|
||||
data = !tree
|
||||
? map2tree(nextState, { key: rootKeyName, pushMethod })
|
||||
: nextState;
|
||||
? // eslint-disable-next-line @typescript-eslint/ban-types
|
||||
(map2tree(nextState as {}, {
|
||||
key: rootKeyName,
|
||||
pushMethod,
|
||||
}) as NodeWithId)
|
||||
: (nextState as NodeWithId);
|
||||
|
||||
if (isEmpty(data) || !data.name) {
|
||||
data = {
|
||||
data = ({
|
||||
name: 'error',
|
||||
message: 'Please provide a state map or a tree structure',
|
||||
};
|
||||
} as unknown) as NodeWithId;
|
||||
}
|
||||
|
||||
let nodeIndex = 0;
|
||||
|
@ -191,13 +288,13 @@ export default function (DOMNode, options = {}) {
|
|||
: null
|
||||
);
|
||||
|
||||
/*eslint-disable*/
|
||||
update();
|
||||
/*eslint-enable*/
|
||||
|
||||
function update() {
|
||||
// path generator for links
|
||||
const diagonal = d3.svg.diagonal().projection((d) => [d.y, d.x]);
|
||||
const diagonal = d3.svg
|
||||
.diagonal<NodePosition>()
|
||||
.projection((d) => [d.y!, d.x!]);
|
||||
// set tree dimensions and spacing between branches and nodes
|
||||
const maxNodeCountByLevel = Math.max(...getNodeGroupByDepthCount(data));
|
||||
|
||||
|
@ -206,12 +303,12 @@ export default function (DOMNode, options = {}) {
|
|||
width,
|
||||
]);
|
||||
|
||||
let nodes = layout.nodes(data);
|
||||
let links = layout.links(nodes);
|
||||
const nodes = layout.nodes(data as d3.layout.tree.Node) as NodeWithId[];
|
||||
const links = layout.links(nodes as d3.layout.tree.Node[]);
|
||||
|
||||
nodes.forEach(
|
||||
(node) =>
|
||||
(node.y = node.depth * (maxLabelLength * 7 * widthBetweenNodesCoeff))
|
||||
(node.y = node.depth! * (maxLabelLength * 7 * widthBetweenNodesCoeff))
|
||||
);
|
||||
|
||||
const nodePositions = nodes.map((n) => ({
|
||||
|
@ -220,15 +317,18 @@ export default function (DOMNode, options = {}) {
|
|||
x: n.x,
|
||||
y: n.y,
|
||||
}));
|
||||
const nodePositionsById = {};
|
||||
const nodePositionsById: { [nodeId: string]: NodePosition } = {};
|
||||
nodePositions.forEach((node) => (nodePositionsById[node.id] = node));
|
||||
|
||||
// process the node selection
|
||||
let node = vis
|
||||
const node = vis
|
||||
.selectAll('g.node')
|
||||
.property('__oldData__', (d) => d)
|
||||
.data(nodes, (d) => d.id || (d.id = ++nodeIndex));
|
||||
let nodeEnter = node
|
||||
.property('__oldData__', (d: NodeWithId) => d)
|
||||
.data(
|
||||
nodes,
|
||||
(d) => d.id || (d.id = (++nodeIndex as unknown) as string)
|
||||
);
|
||||
const nodeEnter = node
|
||||
.enter()
|
||||
.append('g')
|
||||
.attr({
|
||||
|
@ -237,35 +337,39 @@ export default function (DOMNode, options = {}) {
|
|||
const position = findParentNodePosition(
|
||||
nodePositionsById,
|
||||
d.id,
|
||||
(n) => previousNodePositionsById[n.id]
|
||||
(n) => !!previousNodePositionsById[n.id]
|
||||
);
|
||||
const previousPosition =
|
||||
(position && previousNodePositionsById[position.id]) ||
|
||||
previousNodePositionsById.root;
|
||||
return `translate(${previousPosition.y},${previousPosition.x})`;
|
||||
return `translate(${previousPosition.y!},${previousPosition.x!})`;
|
||||
},
|
||||
})
|
||||
.style({
|
||||
fill: style.text.colors.default,
|
||||
cursor: 'pointer',
|
||||
})
|
||||
.on({
|
||||
mouseover: function mouseover() {
|
||||
.on('mouseover', function mouseover(this: any) {
|
||||
d3.select(this).style({
|
||||
fill: style.text.colors.hover,
|
||||
});
|
||||
},
|
||||
mouseout: function mouseout() {
|
||||
})
|
||||
.on('mouseout', function mouseout(this: any) {
|
||||
d3.select(this).style({
|
||||
fill: style.text.colors.default,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!tooltipOptions.disabled) {
|
||||
nodeEnter.call(
|
||||
d3tooltip(d3, 'tooltip', { ...tooltipOptions, root })
|
||||
.text((d, i) => getTooltipString(d, i, tooltipOptions))
|
||||
.text((d, i) =>
|
||||
getTooltipString(
|
||||
d,
|
||||
i,
|
||||
(tooltipOptions as unknown) as { indentationSize: number }
|
||||
)
|
||||
)
|
||||
.style(tooltipOptions.style)
|
||||
);
|
||||
}
|
||||
|
@ -279,12 +383,10 @@ export default function (DOMNode, options = {}) {
|
|||
class: 'nodeCircle',
|
||||
r: 0,
|
||||
})
|
||||
.on({
|
||||
click: (clickedNode) => {
|
||||
if (d3.event.defaultPrevented) return;
|
||||
.on('click', (clickedNode) => {
|
||||
if ((d3.event as Event).defaultPrevented) return;
|
||||
toggleChildren(clickedNode);
|
||||
update();
|
||||
},
|
||||
});
|
||||
|
||||
nodeEnterInnerGroup
|
||||
|
@ -299,9 +401,7 @@ export default function (DOMNode, options = {}) {
|
|||
'fill-opacity': 0,
|
||||
})
|
||||
.text((d) => d.name)
|
||||
.on({
|
||||
click: onClickText,
|
||||
});
|
||||
.on('click', onClickText);
|
||||
|
||||
// update the text to reflect whether node has children or not
|
||||
node.select('text').text((d) => d.name);
|
||||
|
@ -319,11 +419,11 @@ export default function (DOMNode, options = {}) {
|
|||
});
|
||||
|
||||
// transition nodes to their new position
|
||||
let nodeUpdate = node
|
||||
const nodeUpdate = node
|
||||
.transition()
|
||||
.duration(transitionDuration)
|
||||
.attr({
|
||||
transform: (d) => `translate(${d.y},${d.x})`,
|
||||
transform: (d) => `translate(${d.y!},${d.x!})`,
|
||||
});
|
||||
|
||||
// ensure circle radius is correct
|
||||
|
@ -334,7 +434,7 @@ export default function (DOMNode, options = {}) {
|
|||
.select('text')
|
||||
.style('fill-opacity', 1)
|
||||
.attr({
|
||||
transform: function transform(d) {
|
||||
transform: function transform(this: SVGGraphicsElement, d) {
|
||||
const x =
|
||||
(d.children || d._children ? -1 : 1) *
|
||||
(this.getBBox().width / 2 + style.node.radius + 5);
|
||||
|
@ -344,7 +444,7 @@ export default function (DOMNode, options = {}) {
|
|||
|
||||
// blink updated nodes
|
||||
node
|
||||
.filter(function flick(d) {
|
||||
.filter(function flick(this: any, d) {
|
||||
// test whether the relevant properties of d match
|
||||
// the equivalent property of the oldData
|
||||
// also test whether the old data exists,
|
||||
|
@ -358,7 +458,7 @@ export default function (DOMNode, options = {}) {
|
|||
.style('opacity', '1');
|
||||
|
||||
// transition exiting nodes to the parent's new position
|
||||
let nodeExit = node
|
||||
const nodeExit = node
|
||||
.exit()
|
||||
.transition()
|
||||
.duration(transitionDuration)
|
||||
|
@ -367,12 +467,12 @@ export default function (DOMNode, options = {}) {
|
|||
const position = findParentNodePosition(
|
||||
previousNodePositionsById,
|
||||
d.id,
|
||||
(n) => nodePositionsById[n.id]
|
||||
(n) => !!nodePositionsById[n.id]
|
||||
);
|
||||
const futurePosition =
|
||||
(position && nodePositionsById[position.id]) ||
|
||||
nodePositionsById.root;
|
||||
return `translate(${futurePosition.y},${futurePosition.x})`;
|
||||
return `translate(${futurePosition.y!},${futurePosition.x!})`;
|
||||
},
|
||||
})
|
||||
.remove();
|
||||
|
@ -382,7 +482,9 @@ export default function (DOMNode, options = {}) {
|
|||
nodeExit.select('text').style('fill-opacity', 0);
|
||||
|
||||
// update the links
|
||||
let link = vis.selectAll('path.link').data(links, (d) => d.target.id);
|
||||
const link = vis
|
||||
.selectAll('path.link')
|
||||
.data(links, (d) => (d.target as NodeWithId).id);
|
||||
|
||||
// enter any new links at the parent's previous position
|
||||
link
|
||||
|
@ -393,8 +495,8 @@ export default function (DOMNode, options = {}) {
|
|||
d: (d) => {
|
||||
const position = findParentNodePosition(
|
||||
nodePositionsById,
|
||||
d.target.id,
|
||||
(n) => previousNodePositionsById[n.id]
|
||||
(d.target as NodeWithId).id,
|
||||
(n) => !!previousNodePositionsById[n.id]
|
||||
);
|
||||
const previousPosition =
|
||||
(position && previousNodePositionsById[position.id]) ||
|
||||
|
@ -402,14 +504,17 @@ export default function (DOMNode, options = {}) {
|
|||
return diagonal({
|
||||
source: previousPosition,
|
||||
target: previousPosition,
|
||||
});
|
||||
} as d3.svg.diagonal.Link<NodePosition>);
|
||||
},
|
||||
})
|
||||
.style(style.link);
|
||||
|
||||
// transition links to their new position
|
||||
link.transition().duration(transitionDuration).attr({
|
||||
d: diagonal,
|
||||
link
|
||||
.transition()
|
||||
.duration(transitionDuration)
|
||||
.attr({
|
||||
d: (diagonal as unknown) as Primitive,
|
||||
});
|
||||
|
||||
// transition exiting nodes to the parent's new position
|
||||
|
@ -421,8 +526,8 @@ export default function (DOMNode, options = {}) {
|
|||
d: (d) => {
|
||||
const position = findParentNodePosition(
|
||||
previousNodePositionsById,
|
||||
d.target.id,
|
||||
(n) => nodePositionsById[n.id]
|
||||
(d.target as NodeWithId).id,
|
||||
(n) => !!nodePositionsById[n.id]
|
||||
);
|
||||
const futurePosition =
|
||||
(position && nodePositionsById[position.id]) ||
|
|
@ -1,7 +1,8 @@
|
|||
import { is, join, pipe, replace } from 'ramda';
|
||||
import sortAndSerialize from './sortAndSerialize';
|
||||
import { NodeWithId } from './tree';
|
||||
|
||||
export function collapseChildren(node) {
|
||||
export function collapseChildren(node: NodeWithId) {
|
||||
if (node.children) {
|
||||
node._children = node.children;
|
||||
node._children.forEach(collapseChildren);
|
||||
|
@ -9,7 +10,7 @@ export function collapseChildren(node) {
|
|||
}
|
||||
}
|
||||
|
||||
export function expandChildren(node) {
|
||||
export function expandChildren(node: NodeWithId) {
|
||||
if (node._children) {
|
||||
node.children = node._children;
|
||||
node.children.forEach(expandChildren);
|
||||
|
@ -17,7 +18,7 @@ export function expandChildren(node) {
|
|||
}
|
||||
}
|
||||
|
||||
export function toggleChildren(node) {
|
||||
export function toggleChildren(node: NodeWithId) {
|
||||
if (node.children) {
|
||||
node._children = node.children;
|
||||
node.children = null;
|
||||
|
@ -28,16 +29,20 @@ export function toggleChildren(node) {
|
|||
return node;
|
||||
}
|
||||
|
||||
export function visit(parent, visitFn, childrenFn) {
|
||||
export function visit(
|
||||
parent: NodeWithId,
|
||||
visitFn: (parent: NodeWithId) => void,
|
||||
childrenFn: (parent: NodeWithId) => NodeWithId[] | null | undefined
|
||||
) {
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
visitFn(parent);
|
||||
|
||||
let children = childrenFn(parent);
|
||||
const children = childrenFn(parent);
|
||||
if (children) {
|
||||
let count = children.length;
|
||||
const count = children.length;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
visit(children[i], visitFn, childrenFn);
|
||||
|
@ -45,10 +50,10 @@ export function visit(parent, visitFn, childrenFn) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getNodeGroupByDepthCount(rootNode) {
|
||||
let nodeGroupByDepthCount = [1];
|
||||
export function getNodeGroupByDepthCount(rootNode: NodeWithId) {
|
||||
const nodeGroupByDepthCount = [1];
|
||||
|
||||
const traverseFrom = function traverseFrom(node, depth = 0) {
|
||||
const traverseFrom = function traverseFrom(node: NodeWithId, depth = 0) {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -68,7 +73,11 @@ export function getNodeGroupByDepthCount(rootNode) {
|
|||
return nodeGroupByDepthCount;
|
||||
}
|
||||
|
||||
export function getTooltipString(node, i, { indentationSize = 4 }) {
|
||||
export function getTooltipString(
|
||||
node: unknown,
|
||||
i: number | undefined,
|
||||
{ indentationSize = 4 }
|
||||
) {
|
||||
if (!is(Object, node)) return '';
|
||||
|
||||
const spacer = join(' ');
|
||||
|
@ -76,10 +85,13 @@ export function getTooltipString(node, i, { indentationSize = 4 }) {
|
|||
const spaces2nbsp = replace(/\s{2}/g, spacer(new Array(indentationSize)));
|
||||
const json2html = pipe(sortAndSerialize, cr2br, spaces2nbsp);
|
||||
|
||||
const children = node.children || node._children;
|
||||
const children = (node as any).children || (node as any)._children;
|
||||
|
||||
if (typeof node.value !== 'undefined') return json2html(node.value);
|
||||
if (typeof node.object !== 'undefined') return json2html(node.object);
|
||||
if (children && children.length) return 'childrenCount: ' + children.length;
|
||||
if (typeof (node as any).value !== 'undefined')
|
||||
return json2html((node as any).value);
|
||||
if (typeof (node as any).object !== 'undefined')
|
||||
return json2html((node as any).object);
|
||||
if (children && children.length)
|
||||
return `childrenCount: ${(children as unknown[]).length}`;
|
||||
return 'empty';
|
||||
}
|
7
packages/d3-state-visualizer/tsconfig.json
Normal file
7
packages/d3-state-visualizer/tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"extends": "../../tsconfig.react.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
4
packages/d3-state-visualizer/tsconfig.webpack.json
Normal file
4
packages/d3-state-visualizer/tsconfig.webpack.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["webpack.config.umd.ts"]
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ },
|
||||
],
|
||||
},
|
||||
output: {
|
||||
library: 'd3-state-visualizer',
|
||||
libraryExport: 'default',
|
||||
libraryTarget: 'umd',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js'],
|
||||
},
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var webpack = require('webpack');
|
||||
var baseConfig = require('./webpack.config.base');
|
||||
|
||||
var config = Object.assign({}, baseConfig);
|
||||
config.mode = 'development';
|
||||
config.plugins = [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify('development'),
|
||||
}),
|
||||
];
|
||||
|
||||
module.exports = config;
|
|
@ -1,14 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var webpack = require('webpack');
|
||||
var baseConfig = require('./webpack.config.base');
|
||||
|
||||
var config = Object.assign({}, baseConfig);
|
||||
config.mode = 'production';
|
||||
config.plugins = [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||
}),
|
||||
];
|
||||
|
||||
module.exports = config;
|
28
packages/d3-state-visualizer/webpack.config.umd.ts
Normal file
28
packages/d3-state-visualizer/webpack.config.umd.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import * as path from 'path';
|
||||
|
||||
export default (env: { production?: boolean } = {}) => ({
|
||||
mode: env.production ? 'production' : 'development',
|
||||
entry: {
|
||||
app: ['./src/index'],
|
||||
},
|
||||
output: {
|
||||
library: 'd3-state-visualizer',
|
||||
libraryTarget: 'umd',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: env.production
|
||||
? 'd3-state-visualizer.min.js'
|
||||
: 'd3-state-visualizer.js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|ts)$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
});
|
|
@ -34,14 +34,17 @@
|
|||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"type-check": "tsc --noEmit",
|
||||
"type-check:watch": "npm run type-check -- --watch",
|
||||
"preversion": "npm run type-check && npm run lint && npm run test",
|
||||
"preversion": "npm run type-check && npm run lint",
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"ramda": "^0.27.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^3.5.43",
|
||||
"@types/ramda": "^0.27.17"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/d3": "^3.5.43",
|
||||
"d3": "^3.5.17"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,17 @@ import { is } from 'ramda';
|
|||
import utils from './utils';
|
||||
const { prependClass, functor } = utils;
|
||||
|
||||
const defaultOptions = {
|
||||
interface Options<Datum> {
|
||||
left: number | undefined;
|
||||
top: number | undefined;
|
||||
offset: {
|
||||
left: number;
|
||||
top: number;
|
||||
};
|
||||
root: Selection<Datum> | undefined;
|
||||
}
|
||||
|
||||
const defaultOptions: Options<unknown> = {
|
||||
left: undefined, // mouseX
|
||||
top: undefined, // mouseY
|
||||
offset: { left: 0, top: 0 },
|
||||
|
@ -13,9 +23,12 @@ const defaultOptions = {
|
|||
export default function tooltip<Datum>(
|
||||
d3: typeof d3Package,
|
||||
className = 'tooltip',
|
||||
options = {}
|
||||
options: Partial<Options<Datum>> = {}
|
||||
) {
|
||||
const { left, top, offset, root } = { ...defaultOptions, ...options };
|
||||
const { left, top, offset, root } = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
} as Options<Datum>;
|
||||
|
||||
let attrs = { class: className };
|
||||
let text: (datum: Datum, index?: number, outerIndex?: number) => string = (
|
||||
|
@ -44,7 +57,7 @@ export default function tooltip<Datum>(
|
|||
top: `${y}px`,
|
||||
...styles,
|
||||
})
|
||||
.html(() => text(node));
|
||||
.html(() => text(node)) as Selection<Datum>;
|
||||
});
|
||||
|
||||
selection.on('mousemove.tip', (node) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as path from 'path';
|
||||
|
||||
module.exports = (env: { production?: boolean } = {}) => ({
|
||||
export default (env: { production?: boolean } = {}) => ({
|
||||
mode: env.production ? 'production' : 'development',
|
||||
entry: {
|
||||
app: ['./src/index'],
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"type-check": "tsc --noEmit",
|
||||
"type-check:watch": "npm run type-check -- --watch",
|
||||
"preversion": "npm run type-check && npm run lint",
|
||||
"preversion": "npm run type-check && npm run lint && npm run test",
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
@ -2,16 +2,16 @@ import isArray from 'lodash/isArray';
|
|||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
|
||||
interface Node {
|
||||
export interface Node {
|
||||
name: string;
|
||||
children?: Node[];
|
||||
children?: Node[] | null;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
function visit(
|
||||
parent: Node,
|
||||
visitFn: (parent: Node) => void,
|
||||
childrenFn: (parent: Node) => Node[] | undefined
|
||||
childrenFn: (parent: Node) => Node[] | undefined | null
|
||||
) {
|
||||
if (!parent) return;
|
||||
|
||||
|
@ -47,17 +47,18 @@ export default function map2tree(
|
|||
root: {},
|
||||
options: { key?: string; pushMethod?: 'push' | 'unshift' } = {},
|
||||
tree: Node = { name: options.key || 'state', children: [] }
|
||||
): Node {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
): Node | {} {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
if (!isPlainObject(root) && root && !(root as { toJS: () => {} }).toJS) {
|
||||
return {} as Node;
|
||||
return {};
|
||||
}
|
||||
|
||||
const { key: rootNodeKey = 'state', pushMethod = 'push' } = options;
|
||||
const currentNode = getNode(tree, rootNodeKey);
|
||||
|
||||
if (currentNode === null) {
|
||||
return {} as Node;
|
||||
return {};
|
||||
}
|
||||
|
||||
mapValues(
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import map2tree from '../src';
|
||||
import map2tree, { Node } from '../src';
|
||||
import * as immutable from 'immutable';
|
||||
|
||||
test('# rootNodeKey', () => {
|
||||
const map = {};
|
||||
const options = { key: 'foo' };
|
||||
|
||||
expect(map2tree(map, options).name).toBe('foo');
|
||||
expect((map2tree(map, options) as Node).name).toBe('foo');
|
||||
});
|
||||
|
||||
describe('# shallow map', () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as path from 'path';
|
||||
|
||||
module.exports = (env: { production?: boolean } = {}) => ({
|
||||
export default (env: { production?: boolean } = {}) => ({
|
||||
mode: env.production ? 'production' : 'development',
|
||||
entry: {
|
||||
app: ['./src/index'],
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Action, Dispatch } from 'redux';
|
|||
import * as themes from 'redux-devtools-themes';
|
||||
import { Base16Theme } from 'redux-devtools-themes';
|
||||
import { ActionCreators, LiftedAction, LiftedState } from 'redux-devtools';
|
||||
import debounce from 'lodash.debounce';
|
||||
import {
|
||||
updateScrollTop,
|
||||
startConsecutiveToggle,
|
||||
|
@ -12,9 +13,6 @@ import {
|
|||
import reducer, { LogMonitorState } from './reducers';
|
||||
import LogMonitorButtonBar from './LogMonitorButtonBar';
|
||||
import LogMonitorEntryList from './LogMonitorEntryList';
|
||||
import debounce from 'lodash.debounce';
|
||||
import { DockMonitorState } from 'redux-devtools-dock-monitor/lib/reducers';
|
||||
import { DockMonitorAction } from 'redux-devtools-dock-monitor/lib/actions';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const { toggleAction, setActionsActive } = ActionCreators;
|
||||
|
@ -276,8 +274,8 @@ export default (LogMonitor as unknown) as React.ComponentType<
|
|||
> & {
|
||||
update(
|
||||
monitorProps: ExternalProps<unknown, Action<unknown>>,
|
||||
state: DockMonitorState | undefined,
|
||||
action: DockMonitorAction
|
||||
): DockMonitorState;
|
||||
state: LogMonitorState | undefined,
|
||||
action: LogMonitorAction
|
||||
): LogMonitorState;
|
||||
defaultProps: DefaultProps<unknown>;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user