mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2024-11-10 19:56:54 +03:00
Add d3-state-visualizer package (#556)
* Start * Cleanup * Finish * eslint
This commit is contained in:
parent
681b11b500
commit
3022c55330
|
@ -29,6 +29,7 @@
|
|||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"packages/d3-state-visualizer/examples/tree",
|
||||
"packages/react-json-tree/examples",
|
||||
"packages/redux-devtools/examples/counter",
|
||||
"packages/redux-devtools/examples/todomvc",
|
||||
|
|
4
packages/d3-state-visualizer/.babelrc
Normal file
4
packages/d3-state-visualizer/.babelrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-export-default-from"]
|
||||
}
|
83
packages/d3-state-visualizer/README.md
Normal file
83
packages/d3-state-visualizer/README.md
Normal file
|
@ -0,0 +1,83 @@
|
|||
d3-state-visualizer
|
||||
===================
|
||||
Enables real-time visualization of your application state.
|
||||
|
||||
Created by [@romseguy](https://github.com/romseguy) and merged from [`reduxjs/d3-state-visualizer`](https://github.com/reduxjs/d3-state-visualizer).
|
||||
|
||||
[Demo](http://reduxjs.github.io/d3-state-visualizer)
|
||||
|
||||
## Installation
|
||||
|
||||
`yarn install d3-state-visualizer`
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import { tree } from 'd3-state-visualizer';
|
||||
|
||||
const appState = {
|
||||
todoStore: {
|
||||
todos: [
|
||||
{ title: 'd3'},
|
||||
{ title: 'state' },
|
||||
{ title: 'visualizer' },
|
||||
{ title: 'tree' }
|
||||
],
|
||||
completedCount: 1
|
||||
}
|
||||
};
|
||||
|
||||
const render = tree(document.getElementById('root'), {
|
||||
state: appState,
|
||||
id: 'treeExample',
|
||||
size: 1000,
|
||||
aspectRatio: 0.5,
|
||||
isSorted: false,
|
||||
widthBetweenNodesCoeff: 1.5,
|
||||
heightBetweenNodesCoeff: 2,
|
||||
style: {border: '1px solid black'},
|
||||
tooltipOptions: {offset: {left: 30, top: 10}, indentationSize: 2}
|
||||
});
|
||||
|
||||
render();
|
||||
```
|
||||
## Charts API
|
||||
|
||||
The APIs are minimal and consists of a single function you provide with:
|
||||
- a DOM element
|
||||
- a plain old JS object for options.
|
||||
|
||||
#### Tree
|
||||
|
||||
This chart is a bit special as it accepts either one of the two following options, but **not both**:
|
||||
|
||||
- `tree`: a properly formed tree structure such as one generated by [map2tree](https://github.com/reduxjs/redux-devtools/tree/master/packages/map2tree) or [react2tree](https://github.com/romseguy/react2tree)
|
||||
- `state`: a plain javascript object mapping arbitrarily nested keys to values – which will be transformed into a tree structure, again using [map2tree](https://github.com/reduxjs/redux-devtools/tree/master/packages/map2tree).
|
||||
|
||||
Other options are listed below and have reasonable default values if you want to omit them:
|
||||
|
||||
Option | Type | Default | Description
|
||||
--------------------------|----------|-------------|-------------------------------------------------------------------------
|
||||
`id` | String | `'d3svg'` | Sets the identifier of the SVG element —i.e your chart— that will be added to the DOM element you passed as first argument
|
||||
`style` | Object | `{}` | Sets the CSS style of the chart
|
||||
`size` | Number | `500` | Sets size of the chart in pixels
|
||||
`aspectRatio` | Float | `1.0` | Sets the chart height to `size * aspectRatio` and [viewBox](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox) in order to preserve the aspect ratio of the chart. [Great video](https://www.youtube.com/watch?v=FCOeMy7HrBc) if you want to learn more about how SVG works
|
||||
`widthBetweenNodesCoeff` | Float | `1.0` | Alters the horizontal space between each node
|
||||
`heightBetweenNodesCoeff` | Float | `1.0` | Alters the vertical space between each node
|
||||
`isSorted` | Boolean | `false` | Sorts the chart in alphabetical order
|
||||
`transitionDuration` | Number | `750` | Sets the duration of all the transitions used by the chart
|
||||
`tooltipOptions` | Object | [here](https://github.com/reduxjs/redux-devtools/tree/master/packages/d3tooltip) | Sets the options for the [tooltip](https://github.com/reduxjs/redux-devtools/tree/master/packages/d3tooltip) that is showing up when you're hovering the nodes
|
||||
`rootKeyName` | String | `'state'` | Sets the first node's name of the resulting tree structure. **Warning**: only works if you provide a `state` option
|
||||
`pushMethod` | String | `'push'` | Sets the method that shall be used to add array children to the tree. **Warning**: only works if you provide a `state` option
|
||||
|
||||
More to come...
|
||||
|
||||
## Bindings
|
||||
|
||||
### React
|
||||
|
||||
[example](https://github.com/reduxjs/redux-devtools/tree/master/packages/d3-state-visualizer/examples/react-tree) implementation.
|
||||
|
||||
## Roadmap
|
||||
|
||||
* Threshold for large arrays so only a single node is displayed instead of all the children. That single node would be exclude from searching until selected.
|
42
packages/d3-state-visualizer/examples/tree/index.html
Normal file
42
packages/d3-state-visualizer/examples/tree/index.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>State tree with d3-state-visualizer</title>
|
||||
<style type="text/css">
|
||||
|
||||
.node {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nodeCircle {
|
||||
stroke: black;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.nodeText {
|
||||
font-family: sans-serif;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.link {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
font-family: Consolas, Menlo, Monaco, monospace;
|
||||
font-size: 0.8em;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
</div>
|
||||
<script src="/static/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
35
packages/d3-state-visualizer/examples/tree/index.js
vendored
Normal file
35
packages/d3-state-visualizer/examples/tree/index.js
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { tree } from 'd3-state-visualizer';
|
||||
|
||||
const appState = {
|
||||
todoStore: {
|
||||
todos: [
|
||||
{ title: 'd3'},
|
||||
{ title: 'state' },
|
||||
{ title: 'visualizer' },
|
||||
{ title: 'tree' }
|
||||
],
|
||||
completedCount: 1,
|
||||
alphabeticalOrder: true
|
||||
},
|
||||
someStore: {
|
||||
someProperty: 0,
|
||||
someObject: {
|
||||
anotherProperty: 'value',
|
||||
someArray: [0, 1, 2]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const render = tree(document.getElementById('root'), {
|
||||
state: appState,
|
||||
id: 'treeExample',
|
||||
size: 1000,
|
||||
aspectRatio: 0.5,
|
||||
isSorted: false,
|
||||
widthBetweenNodesCoeff: 1.5,
|
||||
heightBetweenNodesCoeff: 2,
|
||||
style: {border: '1px solid black'},
|
||||
tooltipOptions: {offset: {left: 30, top: 10}, indentationSize: 2}
|
||||
});
|
||||
|
||||
render();
|
35
packages/d3-state-visualizer/examples/tree/package.json
Normal file
35
packages/d3-state-visualizer/examples/tree/package.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "d3-state-visualizer-tree-example",
|
||||
"version": "0.0.0",
|
||||
"description": "Visualize your app state as a tree",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/reduxjs/redux-devtools.git"
|
||||
},
|
||||
"keywords": [
|
||||
"d3",
|
||||
"state",
|
||||
"store",
|
||||
"visualization"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/reduxjs/redux-devtools/issues"
|
||||
},
|
||||
"homepage": "https://github.com/reduxjs/redux-devtools",
|
||||
"dependencies": {
|
||||
"d3-state-visualizer": "^1.0.1",
|
||||
"map2tree": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"node-libs-browser": "^0.5.2",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
}
|
||||
}
|
20
packages/d3-state-visualizer/examples/tree/server.js
vendored
Normal file
20
packages/d3-state-visualizer/examples/tree/server.js
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
var webpack = require('webpack');
|
||||
var WebpackDevServer = require('webpack-dev-server');
|
||||
var config = require('./webpack.config');
|
||||
|
||||
new WebpackDevServer(webpack(config), {
|
||||
publicPath: config.output.publicPath,
|
||||
hot: true,
|
||||
historyApiFallback: true,
|
||||
stats: {
|
||||
colors: true
|
||||
}
|
||||
}).listen(3000, 'localhost', function (err) {
|
||||
if (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Listening at localhost:3000');
|
||||
});
|
31
packages/d3-state-visualizer/examples/tree/webpack.config.js
Normal file
31
packages/d3-state-visualizer/examples/tree/webpack.config.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
var path = require('path');
|
||||
var webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
||||
devtool: 'eval-source-map',
|
||||
entry: [
|
||||
'webpack-dev-server/client?http://localhost:3000',
|
||||
'webpack/hot/only-dev-server',
|
||||
'./index'
|
||||
],
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
publicPath: '/static/'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.js']
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js$/,
|
||||
loaders: ['babel-loader'],
|
||||
exclude: /node_modules/,
|
||||
include: __dirname
|
||||
}]
|
||||
}
|
||||
};
|
55
packages/d3-state-visualizer/package.json
Normal file
55
packages/d3-state-visualizer/package.json
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"name": "d3-state-visualizer",
|
||||
"version": "1.3.2",
|
||||
"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",
|
||||
"store",
|
||||
"tree",
|
||||
"visualization"
|
||||
],
|
||||
"author": "romseguy",
|
||||
"license": "MIT",
|
||||
"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.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.10.4",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"babel-loader": "^8.1.0",
|
||||
"rimraf": "^2.7.1",
|
||||
"webpack": "^4.44.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"d3tooltip": "^1.2.2",
|
||||
"deepmerge": "^0.2.10",
|
||||
"is-plain-object": "^2.0.4",
|
||||
"map2tree": "^1.4.1",
|
||||
"ramda": "^0.17.1"
|
||||
}
|
||||
}
|
1
packages/d3-state-visualizer/src/charts/index.js
vendored
Normal file
1
packages/d3-state-visualizer/src/charts/index.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
export tree from './tree/tree';
|
23
packages/d3-state-visualizer/src/charts/tree/sortAndSerialize.js
vendored
Normal file
23
packages/d3-state-visualizer/src/charts/tree/sortAndSerialize.js
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
function sortObject(obj, strict) {
|
||||
if (obj instanceof Array) {
|
||||
let ary
|
||||
if (strict) {
|
||||
ary = obj.sort()
|
||||
} else {
|
||||
ary = obj
|
||||
}
|
||||
return ary
|
||||
}
|
||||
|
||||
if (obj && typeof obj === 'object') {
|
||||
const tObj = {}
|
||||
Object.keys(obj).sort().forEach(key => tObj[key] = sortObject(obj[key]))
|
||||
return tObj
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
export default function sortAndSerialize(obj) {
|
||||
return JSON.stringify(sortObject(obj, true), undefined, 2)
|
||||
}
|
382
packages/d3-state-visualizer/src/charts/tree/tree.js
vendored
Normal file
382
packages/d3-state-visualizer/src/charts/tree/tree.js
vendored
Normal file
|
@ -0,0 +1,382 @@
|
|||
import d3 from 'd3'
|
||||
import { isEmpty } from 'ramda'
|
||||
import map2tree from 'map2tree'
|
||||
import deepmerge from 'deepmerge'
|
||||
import { getTooltipString, toggleChildren, visit, getNodeGroupByDepthCount } from './utils'
|
||||
import d3tooltip from 'd3tooltip'
|
||||
|
||||
const defaultOptions = {
|
||||
state: undefined,
|
||||
rootKeyName: 'state',
|
||||
pushMethod: 'push',
|
||||
tree: undefined,
|
||||
id: 'd3svg',
|
||||
style: {
|
||||
node: {
|
||||
colors: {
|
||||
'default': '#ccc',
|
||||
collapsed: 'lightsteelblue',
|
||||
parent: 'white'
|
||||
},
|
||||
radius: 7
|
||||
},
|
||||
text: {
|
||||
colors: {
|
||||
'default': 'black',
|
||||
hover: 'skyblue'
|
||||
}
|
||||
},
|
||||
link: {
|
||||
stroke: '#000',
|
||||
fill: 'none'
|
||||
}
|
||||
},
|
||||
size: 500,
|
||||
aspectRatio: 1.0,
|
||||
initialZoom: 1,
|
||||
margin: {
|
||||
top: 10,
|
||||
right: 10,
|
||||
bottom: 10,
|
||||
left: 50
|
||||
},
|
||||
isSorted: false,
|
||||
heightBetweenNodesCoeff: 2,
|
||||
widthBetweenNodesCoeff: 1,
|
||||
transitionDuration: 750,
|
||||
blinkDuration: 100,
|
||||
onClickText: () => {},
|
||||
tooltipOptions: {
|
||||
disabled: false,
|
||||
left: undefined,
|
||||
right: undefined,
|
||||
offset: {
|
||||
left: 0,
|
||||
top: 0
|
||||
},
|
||||
style: undefined
|
||||
}
|
||||
}
|
||||
|
||||
export default function(DOMNode, options = {}) {
|
||||
const {
|
||||
id,
|
||||
style,
|
||||
size,
|
||||
aspectRatio,
|
||||
initialZoom,
|
||||
margin,
|
||||
isSorted,
|
||||
widthBetweenNodesCoeff,
|
||||
heightBetweenNodesCoeff,
|
||||
transitionDuration,
|
||||
blinkDuration,
|
||||
state,
|
||||
rootKeyName,
|
||||
pushMethod,
|
||||
tree,
|
||||
tooltipOptions,
|
||||
onClickText
|
||||
} = deepmerge(defaultOptions, options)
|
||||
|
||||
const width = size - margin.left - margin.right
|
||||
const height = size * aspectRatio - margin.top - margin.bottom
|
||||
const fullWidth = size
|
||||
const fullHeight = size * aspectRatio
|
||||
|
||||
const attr = {
|
||||
id,
|
||||
preserveAspectRatio: 'xMinYMin slice'
|
||||
}
|
||||
|
||||
if (!style.width) {
|
||||
attr.width = fullWidth
|
||||
}
|
||||
|
||||
if (!style.width || !style.height) {
|
||||
attr.viewBox = `0 0 ${fullWidth} ${fullHeight}`
|
||||
}
|
||||
|
||||
const root = d3.select(DOMNode)
|
||||
const zoom = d3.behavior.zoom()
|
||||
.scaleExtent([0.1, 3])
|
||||
.scale(initialZoom)
|
||||
const vis = root
|
||||
.append('svg')
|
||||
.attr(attr)
|
||||
.style({cursor: '-webkit-grab', ...style})
|
||||
.call(zoom.on('zoom', () => {
|
||||
const { translate, scale } = d3.event
|
||||
vis.attr('transform', `translate(${translate})scale(${scale})`)
|
||||
}))
|
||||
.append('g')
|
||||
.attr({
|
||||
transform: `translate(${margin.left + style.node.radius}, ${margin.top}) scale(${initialZoom})`
|
||||
})
|
||||
|
||||
let layout = d3.layout.tree().size([width, height])
|
||||
let data
|
||||
|
||||
if (isSorted) {
|
||||
layout.sort((a, b) => b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1)
|
||||
}
|
||||
|
||||
// previousNodePositionsById stores node x and y
|
||||
// as well as hierarchy (id / parentId);
|
||||
// helps animating transitions
|
||||
let previousNodePositionsById = {
|
||||
root: {
|
||||
id: 'root',
|
||||
parentId: null,
|
||||
x: height / 2,
|
||||
y: 0
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
let currentPosition = nodePositionsById[nodeId]
|
||||
while (currentPosition) {
|
||||
currentPosition = nodePositionsById[currentPosition.parentId]
|
||||
if (!currentPosition) {
|
||||
return null
|
||||
}
|
||||
if (!filter || filter(currentPosition)) {
|
||||
return currentPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return function renderChart(nextState = tree || state) {
|
||||
data = !tree ? map2tree(nextState, {key: rootKeyName, pushMethod}) : nextState
|
||||
|
||||
if (isEmpty(data) || !data.name) {
|
||||
data = { name: 'error', message: 'Please provide a state map or a tree structure'}
|
||||
}
|
||||
|
||||
let nodeIndex = 0
|
||||
let maxLabelLength = 0
|
||||
|
||||
// nodes are assigned with string ids, which reflect their location
|
||||
// within the hierarcy; e.g. "root|branch|subBranch|subBranch[0]|property"
|
||||
// top-level elemnt always has id "root"
|
||||
visit(data,
|
||||
node => {
|
||||
maxLabelLength = Math.max(node.name.length, maxLabelLength)
|
||||
node.id = node.id || 'root'
|
||||
},
|
||||
node => node.children && node.children.length > 0 ? node.children.map((c) => {
|
||||
c.id = `${node.id || ''}|${c.name}`
|
||||
return c
|
||||
}) : null
|
||||
)
|
||||
|
||||
/*eslint-disable*/
|
||||
update()
|
||||
/*eslint-enable*/
|
||||
|
||||
function update() {
|
||||
// path generator for links
|
||||
const diagonal = d3.svg.diagonal().projection(d => [d.y, d.x])
|
||||
// set tree dimensions and spacing between branches and nodes
|
||||
const maxNodeCountByLevel = Math.max(...getNodeGroupByDepthCount(data))
|
||||
|
||||
layout = layout.size([maxNodeCountByLevel * 25 * heightBetweenNodesCoeff, width])
|
||||
|
||||
let nodes = layout.nodes(data)
|
||||
let links = layout.links(nodes)
|
||||
|
||||
nodes.forEach(node => node.y = node.depth * (maxLabelLength * 7 * widthBetweenNodesCoeff))
|
||||
|
||||
const nodePositions = nodes.map(n => ({
|
||||
parentId: n.parent && n.parent.id,
|
||||
id: n.id,
|
||||
x: n.x,
|
||||
y: n.y
|
||||
}))
|
||||
const nodePositionsById = {}
|
||||
nodePositions.forEach(node => nodePositionsById[node.id] = node)
|
||||
|
||||
// process the node selection
|
||||
let node = vis.selectAll('g.node')
|
||||
.property('__oldData__', d => d)
|
||||
.data(nodes, d => d.id || (d.id = ++nodeIndex))
|
||||
let nodeEnter = node.enter().append('g')
|
||||
.attr({
|
||||
'class': 'node',
|
||||
transform: d => {
|
||||
const position = findParentNodePosition(nodePositionsById, d.id, (n) => previousNodePositionsById[n.id])
|
||||
const previousPosition = position && previousNodePositionsById[position.id] || previousNodePositionsById.root
|
||||
return `translate(${previousPosition.y},${previousPosition.x})`
|
||||
}
|
||||
})
|
||||
.style({
|
||||
fill: style.text.colors.default,
|
||||
cursor: 'pointer'
|
||||
})
|
||||
.on({
|
||||
mouseover: function mouseover() {
|
||||
d3.select(this).style({
|
||||
fill: style.text.colors.hover
|
||||
})
|
||||
},
|
||||
mouseout: function mouseout() {
|
||||
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))
|
||||
.style(tooltipOptions.style)
|
||||
)
|
||||
}
|
||||
|
||||
// g inside node contains circle and text
|
||||
// this extra wrapper helps run d3 transitions in parallel
|
||||
const nodeEnterInnerGroup = nodeEnter.append('g')
|
||||
nodeEnterInnerGroup.append('circle')
|
||||
.attr({
|
||||
'class': 'nodeCircle',
|
||||
r: 0
|
||||
})
|
||||
.on({
|
||||
click: clickedNode => {
|
||||
if (d3.event.defaultPrevented) return
|
||||
toggleChildren(clickedNode)
|
||||
update()
|
||||
}
|
||||
})
|
||||
|
||||
nodeEnterInnerGroup.append('text')
|
||||
.attr({
|
||||
'class': 'nodeText',
|
||||
'text-anchor': 'middle',
|
||||
'transform': `translate(0,0)`,
|
||||
dy: '.35em'
|
||||
})
|
||||
.style({
|
||||
'fill-opacity': 0
|
||||
})
|
||||
.text(d => d.name)
|
||||
.on({
|
||||
click: onClickText
|
||||
})
|
||||
|
||||
// update the text to reflect whether node has children or not
|
||||
node.select('text')
|
||||
.text(d => d.name)
|
||||
|
||||
// change the circle fill depending on whether it has children and is collapsed
|
||||
node.select('circle')
|
||||
.style({
|
||||
stroke: 'black',
|
||||
'stroke-width': '1.5px',
|
||||
fill: d => d._children ? style.node.colors.collapsed : (d.children ? style.node.colors.parent : style.node.colors.default)
|
||||
})
|
||||
|
||||
// transition nodes to their new position
|
||||
let nodeUpdate = node.transition()
|
||||
.duration(transitionDuration)
|
||||
.attr({
|
||||
transform: d => `translate(${d.y},${d.x})`
|
||||
})
|
||||
|
||||
// ensure circle radius is correct
|
||||
nodeUpdate.select('circle')
|
||||
.attr('r', style.node.radius)
|
||||
|
||||
// fade the text in and align it
|
||||
nodeUpdate.select('text')
|
||||
.style('fill-opacity', 1)
|
||||
.attr({
|
||||
transform: function transform(d) {
|
||||
const x = (d.children || d._children ? -1 : 1) * (this.getBBox().width / 2 + style.node.radius + 5)
|
||||
return `translate(${x},0)`
|
||||
}
|
||||
})
|
||||
|
||||
// blink updated nodes
|
||||
node.filter(function flick(d) {
|
||||
// test whether the relevant properties of d match
|
||||
// the equivalent property of the oldData
|
||||
// also test whether the old data exists,
|
||||
// to catch the entering elements!
|
||||
return (this.__oldData__ && d.value !== this.__oldData__.value)
|
||||
})
|
||||
.select('g')
|
||||
.style('opacity', '0.3').transition()
|
||||
.duration(blinkDuration).style('opacity', '1')
|
||||
|
||||
// transition exiting nodes to the parent's new position
|
||||
let nodeExit = node.exit().transition()
|
||||
.duration(transitionDuration)
|
||||
.attr({
|
||||
transform: d => {
|
||||
const position = findParentNodePosition(previousNodePositionsById, d.id, (n) => nodePositionsById[n.id])
|
||||
const futurePosition = position && nodePositionsById[position.id] || nodePositionsById.root
|
||||
return `translate(${futurePosition.y},${futurePosition.x})`
|
||||
}
|
||||
})
|
||||
.remove()
|
||||
|
||||
nodeExit.select('circle')
|
||||
.attr('r', 0)
|
||||
|
||||
nodeExit.select('text')
|
||||
.style('fill-opacity', 0)
|
||||
|
||||
// update the links
|
||||
let link = vis.selectAll('path.link')
|
||||
.data(links, d => d.target.id)
|
||||
|
||||
// enter any new links at the parent's previous position
|
||||
link.enter().insert('path', 'g')
|
||||
.attr({
|
||||
'class': 'link',
|
||||
d: d => {
|
||||
const position = findParentNodePosition(nodePositionsById, d.target.id, (n) => previousNodePositionsById[n.id])
|
||||
const previousPosition = position && previousNodePositionsById[position.id] || previousNodePositionsById.root
|
||||
return diagonal({
|
||||
source: previousPosition,
|
||||
target: previousPosition
|
||||
})
|
||||
}
|
||||
})
|
||||
.style(style.link)
|
||||
|
||||
// transition links to their new position
|
||||
link.transition()
|
||||
.duration(transitionDuration)
|
||||
.attr({
|
||||
d: diagonal
|
||||
})
|
||||
|
||||
// transition exiting nodes to the parent's new position
|
||||
link.exit()
|
||||
.transition()
|
||||
.duration(transitionDuration)
|
||||
.attr({
|
||||
d: d => {
|
||||
const position = findParentNodePosition(previousNodePositionsById, d.target.id, (n) => nodePositionsById[n.id])
|
||||
const futurePosition = position && nodePositionsById[position.id] || nodePositionsById.root
|
||||
return diagonal({
|
||||
source: futurePosition,
|
||||
target: futurePosition
|
||||
})
|
||||
}
|
||||
})
|
||||
.remove()
|
||||
|
||||
// delete the old data once it's no longer needed
|
||||
node.property('__oldData__', null)
|
||||
|
||||
// stash the old positions for transition
|
||||
previousNodePositionsById = nodePositionsById
|
||||
}
|
||||
}
|
||||
}
|
85
packages/d3-state-visualizer/src/charts/tree/utils.js
vendored
Normal file
85
packages/d3-state-visualizer/src/charts/tree/utils.js
vendored
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { is, join, pipe, replace } from 'ramda';
|
||||
import sortAndSerialize from './sortAndSerialize';
|
||||
|
||||
export function collapseChildren(node) {
|
||||
if (node.children) {
|
||||
node._children = node.children;
|
||||
node._children.forEach(collapseChildren);
|
||||
node.children = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function expandChildren(node) {
|
||||
if (node._children) {
|
||||
node.children = node._children;
|
||||
node.children.forEach(expandChildren);
|
||||
node._children = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleChildren(node) {
|
||||
if (node.children) {
|
||||
node._children = node.children;
|
||||
node.children = null;
|
||||
} else if (node._children) {
|
||||
node.children = node._children;
|
||||
node._children = null;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
export function visit(parent, visitFn, childrenFn) {
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
visitFn(parent);
|
||||
|
||||
let children = childrenFn(parent);
|
||||
if (children) {
|
||||
let count = children.length;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
visit(children[i], visitFn, childrenFn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getNodeGroupByDepthCount(rootNode) {
|
||||
let nodeGroupByDepthCount = [1];
|
||||
|
||||
const traverseFrom = function traverseFrom(node, depth = 0) {
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (nodeGroupByDepthCount.length <= depth + 1) {
|
||||
nodeGroupByDepthCount.push(0);
|
||||
}
|
||||
|
||||
nodeGroupByDepthCount[depth + 1] += node.children.length;
|
||||
|
||||
node.children.forEach(childNode => {
|
||||
traverseFrom(childNode, depth + 1);
|
||||
});
|
||||
};
|
||||
|
||||
traverseFrom(rootNode);
|
||||
return nodeGroupByDepthCount;
|
||||
}
|
||||
|
||||
export function getTooltipString(node, i, { indentationSize = 4 }) {
|
||||
if (!is(Object, node)) return '';
|
||||
|
||||
const spacer = join(' ');
|
||||
const cr2br = replace(/\n/g, '<br/>');
|
||||
const spaces2nbsp = replace(/\s{2}/g, spacer(new Array(indentationSize)));
|
||||
const json2html = pipe(sortAndSerialize, cr2br, spaces2nbsp);
|
||||
|
||||
const children = node.children || node._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;
|
||||
return 'empty';
|
||||
}
|
5
packages/d3-state-visualizer/src/index.js
vendored
Normal file
5
packages/d3-state-visualizer/src/index.js
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
import * as charts from './charts';
|
||||
|
||||
export { tree } from './charts';
|
||||
|
||||
export default charts;
|
16
packages/d3-state-visualizer/webpack.config.base.js
Normal file
16
packages/d3-state-visualizer/webpack.config.base.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ }
|
||||
]
|
||||
},
|
||||
output: {
|
||||
library: 'd3-state-visualizer',
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js']
|
||||
}
|
||||
};
|
14
packages/d3-state-visualizer/webpack.config.development.js
Normal file
14
packages/d3-state-visualizer/webpack.config.development.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
'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;
|
14
packages/d3-state-visualizer/webpack.config.production.js
Normal file
14
packages/d3-state-visualizer/webpack.config.production.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
'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;
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "map2tree",
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.1",
|
||||
"description": "Utility for mapping maps to trees",
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
Redux DevTools Chart Monitor
|
||||
=========================
|
||||
|
||||
A chart monitor for [Redux DevTools](https://github.com/gaearon/redux-devtools). Created by [@romseguy](https://github.com/romseguy) and merged from [`reduxjs/redux-devtools-chart-monitor`](https://github.com/reduxjs/redux-devtools-chart-monitor).
|
||||
A chart monitor for [Redux DevTools](https://github.com/gaearon/redux-devtools).
|
||||
|
||||
Created by [@romseguy](https://github.com/romseguy) and merged from [`reduxjs/redux-devtools-chart-monitor`](https://github.com/reduxjs/redux-devtools-chart-monitor).
|
||||
|
||||
It shows a real-time view of the store aka the current state of the app.
|
||||
|
||||
|
|
26
yarn.lock
26
yarn.lock
|
@ -5860,19 +5860,7 @@ cyclist@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
|
||||
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
|
||||
|
||||
d3-state-visualizer@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-state-visualizer/-/d3-state-visualizer-1.3.2.tgz#8e3ac418aa7ee7e3f46025309f9d1c215ee385eb"
|
||||
integrity sha512-XgTRC6FXeoTt8l79cc2f3Zaah+K7DUQb3GL0zfbvoIi7zWWHV4l7OfuX9/JxxvwilKApMZwHMBJ7cJ2yWAc5IQ==
|
||||
dependencies:
|
||||
d3 "^3.5.6"
|
||||
d3tooltip "^1.2.2"
|
||||
deepmerge "^0.2.10"
|
||||
is-plain-object "2.0.1"
|
||||
map2tree "^1.4.0"
|
||||
ramda "^0.17.1"
|
||||
|
||||
d3@^3.5.6:
|
||||
d3@^3.5.17:
|
||||
version "3.5.17"
|
||||
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
|
||||
integrity sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=
|
||||
|
@ -9230,13 +9218,6 @@ is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
|
||||
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
|
||||
|
||||
is-plain-object@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.1.tgz#4d7ca539bc9db9b737b8acb612f2318ef92f294f"
|
||||
integrity sha1-TXylObydubc3uKy2EvIxjvkvKU8=
|
||||
dependencies:
|
||||
isobject "^1.0.0"
|
||||
|
||||
is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
|
||||
|
@ -9396,11 +9377,6 @@ isexe@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||
|
||||
isobject@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-1.0.2.tgz#f0f9b8ce92dd540fa0740882e3835a2e022ec78a"
|
||||
integrity sha1-8Pm4zpLdVA+gdAiC44NaLgIux4o=
|
||||
|
||||
isobject@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
|
||||
|
|
Loading…
Reference in New Issue
Block a user