diff --git a/package.json b/package.json
index e22a9a1ed6..33ae291d7a 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/packages/d3-state-visualizer/.babelrc b/packages/d3-state-visualizer/.babelrc
new file mode 100644
index 0000000000..645cc56ee1
--- /dev/null
+++ b/packages/d3-state-visualizer/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["@babel/preset-env", "@babel/preset-react"],
+ "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-export-default-from"]
+}
diff --git a/packages/d3-state-visualizer/README.md b/packages/d3-state-visualizer/README.md
new file mode 100644
index 0000000000..6addc6a424
--- /dev/null
+++ b/packages/d3-state-visualizer/README.md
@@ -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.
diff --git a/packages/d3-state-visualizer/examples/tree/index.html b/packages/d3-state-visualizer/examples/tree/index.html
new file mode 100644
index 0000000000..4fea9f2256
--- /dev/null
+++ b/packages/d3-state-visualizer/examples/tree/index.html
@@ -0,0 +1,42 @@
+
+
+ State tree with d3-state-visualizer
+
+
+
+
+
+
+
+
diff --git a/packages/d3-state-visualizer/examples/tree/index.js b/packages/d3-state-visualizer/examples/tree/index.js
new file mode 100644
index 0000000000..90eb327f19
--- /dev/null
+++ b/packages/d3-state-visualizer/examples/tree/index.js
@@ -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();
diff --git a/packages/d3-state-visualizer/examples/tree/package.json b/packages/d3-state-visualizer/examples/tree/package.json
new file mode 100644
index 0000000000..2d852792eb
--- /dev/null
+++ b/packages/d3-state-visualizer/examples/tree/package.json
@@ -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"
+ }
+}
diff --git a/packages/d3-state-visualizer/examples/tree/server.js b/packages/d3-state-visualizer/examples/tree/server.js
new file mode 100644
index 0000000000..e309a0b3b5
--- /dev/null
+++ b/packages/d3-state-visualizer/examples/tree/server.js
@@ -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');
+});
diff --git a/packages/d3-state-visualizer/examples/tree/webpack.config.js b/packages/d3-state-visualizer/examples/tree/webpack.config.js
new file mode 100644
index 0000000000..d99e1fcd1c
--- /dev/null
+++ b/packages/d3-state-visualizer/examples/tree/webpack.config.js
@@ -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
+ }]
+ }
+};
diff --git a/packages/d3-state-visualizer/package.json b/packages/d3-state-visualizer/package.json
new file mode 100644
index 0000000000..db6acbe5ee
--- /dev/null
+++ b/packages/d3-state-visualizer/package.json
@@ -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"
+ }
+}
diff --git a/packages/d3-state-visualizer/src/charts/index.js b/packages/d3-state-visualizer/src/charts/index.js
new file mode 100644
index 0000000000..d2f1835eeb
--- /dev/null
+++ b/packages/d3-state-visualizer/src/charts/index.js
@@ -0,0 +1 @@
+export tree from './tree/tree';
diff --git a/packages/d3-state-visualizer/src/charts/tree/sortAndSerialize.js b/packages/d3-state-visualizer/src/charts/tree/sortAndSerialize.js
new file mode 100644
index 0000000000..1dc5afb625
--- /dev/null
+++ b/packages/d3-state-visualizer/src/charts/tree/sortAndSerialize.js
@@ -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)
+}
diff --git a/packages/d3-state-visualizer/src/charts/tree/tree.js b/packages/d3-state-visualizer/src/charts/tree/tree.js
new file mode 100644
index 0000000000..a32cbeaea6
--- /dev/null
+++ b/packages/d3-state-visualizer/src/charts/tree/tree.js
@@ -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
+ }
+ }
+}
diff --git a/packages/d3-state-visualizer/src/charts/tree/utils.js b/packages/d3-state-visualizer/src/charts/tree/utils.js
new file mode 100644
index 0000000000..96d24bb373
--- /dev/null
+++ b/packages/d3-state-visualizer/src/charts/tree/utils.js
@@ -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, '
');
+ 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';
+}
diff --git a/packages/d3-state-visualizer/src/index.js b/packages/d3-state-visualizer/src/index.js
new file mode 100644
index 0000000000..78a37ac164
--- /dev/null
+++ b/packages/d3-state-visualizer/src/index.js
@@ -0,0 +1,5 @@
+import * as charts from './charts';
+
+export { tree } from './charts';
+
+export default charts;
diff --git a/packages/d3-state-visualizer/webpack.config.base.js b/packages/d3-state-visualizer/webpack.config.base.js
new file mode 100644
index 0000000000..a15e2e76f7
--- /dev/null
+++ b/packages/d3-state-visualizer/webpack.config.base.js
@@ -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']
+ }
+};
diff --git a/packages/d3-state-visualizer/webpack.config.development.js b/packages/d3-state-visualizer/webpack.config.development.js
new file mode 100644
index 0000000000..703f597b2c
--- /dev/null
+++ b/packages/d3-state-visualizer/webpack.config.development.js
@@ -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;
diff --git a/packages/d3-state-visualizer/webpack.config.production.js b/packages/d3-state-visualizer/webpack.config.production.js
new file mode 100644
index 0000000000..24b19db40d
--- /dev/null
+++ b/packages/d3-state-visualizer/webpack.config.production.js
@@ -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;
diff --git a/packages/map2tree/package.json b/packages/map2tree/package.json
index 23685e209f..ad52cd081c 100755
--- a/packages/map2tree/package.json
+++ b/packages/map2tree/package.json
@@ -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": {
diff --git a/packages/redux-devtools-chart-monitor/README.md b/packages/redux-devtools-chart-monitor/README.md
index b70efd0bba..e421f7309e 100644
--- a/packages/redux-devtools-chart-monitor/README.md
+++ b/packages/redux-devtools-chart-monitor/README.md
@@ -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.
diff --git a/yarn.lock b/yarn.lock
index 1855327d2a..75bdd3fc00 100644
--- a/yarn.lock
+++ b/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"