Add d3-state-visualizer package (#556)

* Start

* Cleanup

* Finish

* eslint
This commit is contained in:
Nathan Bierema 2020-08-05 08:42:25 -04:00 committed by GitHub
parent 681b11b500
commit 3022c55330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 851 additions and 27 deletions

View File

@ -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",

View File

@ -0,0 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-export-default-from"]
}

View 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.

View 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>

View 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();

View 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"
}
}

View 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');
});

View 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
}]
}
};

View 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"
}
}

View File

@ -0,0 +1 @@
export tree from './tree/tree';

View 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)
}

View 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
}
}
}

View 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('&nbsp;&nbsp;');
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';
}

View File

@ -0,0 +1,5 @@
import * as charts from './charts';
export { tree } from './charts';
export default charts;

View 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']
}
};

View 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;

View 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;

View File

@ -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": {

View File

@ -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.

View File

@ -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"