From 3022c55330749150640546bee5b32b69447102bc Mon Sep 17 00:00:00 2001 From: Nathan Bierema Date: Wed, 5 Aug 2020 08:42:25 -0400 Subject: [PATCH] Add d3-state-visualizer package (#556) * Start * Cleanup * Finish * eslint --- package.json | 1 + packages/d3-state-visualizer/.babelrc | 4 + packages/d3-state-visualizer/README.md | 83 ++++ .../examples/tree/index.html | 42 ++ .../examples/tree/index.js | 35 ++ .../examples/tree/package.json | 35 ++ .../examples/tree/server.js | 20 + .../examples/tree/webpack.config.js | 31 ++ packages/d3-state-visualizer/package.json | 55 +++ .../d3-state-visualizer/src/charts/index.js | 1 + .../src/charts/tree/sortAndSerialize.js | 23 ++ .../src/charts/tree/tree.js | 382 ++++++++++++++++++ .../src/charts/tree/utils.js | 85 ++++ packages/d3-state-visualizer/src/index.js | 5 + .../webpack.config.base.js | 16 + .../webpack.config.development.js | 14 + .../webpack.config.production.js | 14 + packages/map2tree/package.json | 2 +- .../redux-devtools-chart-monitor/README.md | 4 +- yarn.lock | 26 +- 20 files changed, 851 insertions(+), 27 deletions(-) create mode 100644 packages/d3-state-visualizer/.babelrc create mode 100644 packages/d3-state-visualizer/README.md create mode 100644 packages/d3-state-visualizer/examples/tree/index.html create mode 100644 packages/d3-state-visualizer/examples/tree/index.js create mode 100644 packages/d3-state-visualizer/examples/tree/package.json create mode 100644 packages/d3-state-visualizer/examples/tree/server.js create mode 100644 packages/d3-state-visualizer/examples/tree/webpack.config.js create mode 100644 packages/d3-state-visualizer/package.json create mode 100644 packages/d3-state-visualizer/src/charts/index.js create mode 100644 packages/d3-state-visualizer/src/charts/tree/sortAndSerialize.js create mode 100644 packages/d3-state-visualizer/src/charts/tree/tree.js create mode 100644 packages/d3-state-visualizer/src/charts/tree/utils.js create mode 100644 packages/d3-state-visualizer/src/index.js create mode 100644 packages/d3-state-visualizer/webpack.config.base.js create mode 100644 packages/d3-state-visualizer/webpack.config.development.js create mode 100644 packages/d3-state-visualizer/webpack.config.production.js diff --git a/package.json b/package.json index e22a9a1e..33ae291d 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 00000000..645cc56e --- /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 00000000..6addc6a4 --- /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 00000000..4fea9f22 --- /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 00000000..90eb327f --- /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 00000000..2d852792 --- /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 00000000..e309a0b3 --- /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 00000000..d99e1fcd --- /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 00000000..db6acbe5 --- /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 00000000..d2f1835e --- /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 00000000..1dc5afb6 --- /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 00000000..a32cbeae --- /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 00000000..96d24bb3 --- /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 00000000..78a37ac1 --- /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 00000000..a15e2e76 --- /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 00000000..703f597b --- /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 00000000..24b19db4 --- /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 23685e20..ad52cd08 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 b70efd0b..e421f730 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 1855327d..75bdd3fc 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"