Related
I am trying to create a d3 force directed network graph where based on a given network I can update the network by modifying the links and nodes and re-update them in the svg.
I tweaked the code so that a g element can be created which enclose each node circle so that I can add a text inside that same g element.
But now the labels are working perfectly but when I transition from graph 3 to graph 1 by clicking button 3 first then clicking button 1, the links that are redundant (a->d, a->f) are removed perfectly but the nodes that are redundant (e and f) stays in the svg.
I could not figure out whether it was a wrong selection or do I need some tweaking in the tick() function?
Here is the code:
var height = 200;
var width = 200;
const graph = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 }
]
}
const graph2 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 }
]
}
const graph3 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 },
{ "name": "e", "group": 4 },
{ "name": "f", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 },
{ "source": "f", "target": "a", "value": 1 }
]
}
var simulation = d3.forceSimulation()
.force("ct", d3.forceCenter(height / 2, width / 2))
.force("link", d3.forceLink().id(function(d) { return d.name; })
.distance(50).strength(2))
.force("charge", d3.forceManyBody().strength(-240))
// use forceX and forceY instead to change the relative positioning
// .force("centering", d3.forceCenter(width/2, height/2))
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(height / 2))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
function start(graph) {
var linkElements = svg.select(".links").selectAll(".link").data(graph.links);
linkElements.enter().append("line").attr("class", "link");
linkElements.exit().remove();
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
.enter().append("g")
.attr("class", "node");
var circles = nodeElements.append("circle")
.attr("r", 8);
var labels = nodeElements.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
nodeElements.exit().remove();
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
simulation.alphaTarget(0.1).restart();
}
function tick() {
var nodeElements = svg.select(".nodes").selectAll(".node");
var linkElements = svg.select(".links").selectAll(".link");
nodeElements.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
linkElements.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
start(graph);
document.getElementById('btn1').addEventListener('click', function() {
start(graph);
});
document.getElementById('btn2').addEventListener('click', function() {
start(graph2);
});
document.getElementById('btn3').addEventListener('click', function() {
start(graph3);
});
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
stroke-width: 1.5px;
}
text {
font-family: sans-serif;
font-size: 10px;
fill: #000000;
}
<body>
<div>
<button id='btn1'>1</button>
<button id='btn2'>2</button>
<button id='btn3'>3</button>
</div>
</body>
<script src="https://d3js.org/d3.v5.min.js"></script>
Here is the jsfiddle version of the code: https://jsfiddle.net/syedarehaq/myd0h5w1/
In the provided code, variable nodeElements contains the enter selection, rather than the whole data binding selection.
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
.enter().append("g")
.attr("class", "node");
nodeElements declaration should not contain the .enter bit - it should be just like linkSelection variable declaration:
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
Then, in order to append the new circles and texts only to the entering g elements, adapt as follows:
var enterSelection = nodeElements.enter().append("g")
.attr("class", "node");
var circles = enterSelection.append("circle")
.attr("r", 8);
var labels = enterSelection.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
The exit function call is now working as expected.
Demo in the snippet below.
var height = 200;
var width = 200;
const graph = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 }
]
}
const graph2 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 }
]
}
const graph3 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 },
{ "name": "e", "group": 4 },
{ "name": "f", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 },
{ "source": "f", "target": "a", "value": 1 }
]
}
var simulation = d3.forceSimulation()
.force("ct", d3.forceCenter(height / 2, width / 2))
.force("link", d3.forceLink().id(function(d) { return d.name; })
.distance(50).strength(2))
.force("charge", d3.forceManyBody().strength(-240))
// use forceX and forceY instead to change the relative positioning
// .force("centering", d3.forceCenter(width/2, height/2))
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(height / 2))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
function start(graph) {
var linkElements = svg.select(".links").selectAll(".link").data(graph.links);
linkElements.enter().append("line").attr("class", "link");
linkElements.exit().remove();
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
var enterSelection = nodeElements.enter().append("g")
.attr("class", "node");
var circles = enterSelection.append("circle")
.attr("r", 8);
var labels = enterSelection.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
nodeElements.exit().remove();
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
simulation.alphaTarget(0.1).restart();
}
function tick() {
var nodeElements = svg.select(".nodes").selectAll(".node");
var linkElements = svg.select(".links").selectAll(".link");
nodeElements.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
linkElements.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
start(graph);
document.getElementById('btn1').addEventListener('click', function() {
start(graph);
});
document.getElementById('btn2').addEventListener('click', function() {
start(graph2);
});
document.getElementById('btn3').addEventListener('click', function() {
start(graph3);
});
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
stroke-width: 1.5px;
}
text {
font-family: sans-serif;
font-size: 10px;
fill: #000000;
}
<body>
<div>
<button id='btn1'>1</button>
<button id='btn2'>2</button>
<button id='btn3'>3</button>
</div>
</body>
<script src="https://d3js.org/d3.v5.min.js"></script>
I am trying to make a sunburst by following the 3-part tutorial on https://bl.ocks.org/denjn5/3b74baf5edc4ac93d5e487136481c601 My json contains sell information based on country and product division. I am trying to show in the first layer sell based on country and in the 2nd layer sell based on product division. My Json file looks like this:
{
"country": "All",
"shares":[
{
"country": "at",
"shares":[
{
"productdivision": "accessorie",
"label": 53222
},
{
"productdivision": "apparel",
"label": 365712
},
{
"productdivision": "footwear",
"label": 523684
}
]
},
{
"country": "be",
"shares":[
{
"productdivision": "accessorie",
"label": 57522
},
{
"productdivision": "apparel",
"label": 598712
},
{
"productdivision": "footwear",
"label": 52284
}
]
},
{
"country": "DE",
"shares":[
{
"productdivision": "accessorie",
"label": 56982
},
{
"productdivision": "apparel",
"label": 55312
},
{
"productdivision": "footwear",
"label": 67284
}
]
},
{
"country": "Fr",
"shares":[
{
"productdivision": "accessorie",
"label": 5862
},
{
"productdivision": "apparel",
"label": 45312
},
{
"productdivision": "footwear",
"label": 26284
}
]
}
]
}
This json file's name is kpiDrillDown2.json and I call it in my code with d3.json(). I have made slight changes to the code to work for my data. The code is as follows:
<html>
<head>
<style>
#import url('https://fonts.googleapis.com/css?family=Raleway');
body {
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<svg></svg>
<script>
//initialize variables
var width = 500;
var height = 500;
var radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory20b);
//setting up svg workspace
var g = d3.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
//formatting the data
var partition = d3.partition()
.size([2 * Math.PI, radius]);
function draw(nodeData){
debugger;
//finding the root node
var root = d3.hierarchy(nodeData)
.sum(function (d) { return d.label});
//calculating each arc
partition(root);
var arc = d3.arc()
.startAngle(function (d) { return d.x0; })
.endAngle(function (d) { return d.x1; })
.innerRadius(function (d) { return d.y0; })
.outerRadius(function (d) { return d.y1; });
g.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr("class", "node")
.append('path')
.attr("display", function (d) { return d.depth ? null : "none"; })
.attr("d", arc)
.style('stroke', '#fff')
.style("fill", function (d) { return color((d.parent ? d : d.parent).data.productdivision); })
g.selectAll(".node")
.append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")rotate(" + computeTextRotation(d) + ")"; })
.attr("dx", "-20")
.attr("dy", ".5em")
.text(function(d) { return d.parent ? d.data.productdivision : "" });
function computeTextRotation(d) {
var angle = (d.x0 + d.x1) / Math.PI * 90;
// Avoid upside-down labels
return (angle < 90 || angle > 270) ? angle : angle + 180;
}
}
d3.json('kpiDrillDown3.json', draw);
</script>
</body>
</html>
I put a debbuger in the draw functin to inspect root element. Root doesn't have any children. This is what I see in the console:
When I continue it gives me the error:"Cannot read property 'data' of null". As shown in console, root doesn't have children. My question is, do I need to change my json data format to make root recogninze the chilren, or am I doing something wrong. I am new to d3js and basically by getting the source code and modifying it, I am making my way through. This is the error in console:
I appreciate your help and thank you very much.
According to the API:
The specified children accessor function is invoked for each datum, starting with the root data, and must return an array of data representing the children, or null if the current datum has no children. If children is not specified, it defaults to:
function children(d) {
return d.children;
}
However, in your data structure, you don't have children, but shares instead.
So, the hierarchy should be:
var root = d3.hierarchy(data, function(d) {
return d.shares;
})
Pay attention to the fact that in the JSON of that tutorial you linked (just like in the API's example) the children's array is named children.
Here is a demo, look at the console (your browser's console, not the snippet one):
var data = {
"country": "All",
"shares": [{
"country": "at",
"shares": [{
"productdivision": "accessorie",
"label": 53222
},
{
"productdivision": "apparel",
"label": 365712
},
{
"productdivision": "footwear",
"label": 523684
}
]
},
{
"country": "be",
"shares": [{
"productdivision": "accessorie",
"label": 57522
},
{
"productdivision": "apparel",
"label": 598712
},
{
"productdivision": "footwear",
"label": 52284
}
]
},
{
"country": "DE",
"shares": [{
"productdivision": "accessorie",
"label": 56982
},
{
"productdivision": "apparel",
"label": 55312
},
{
"productdivision": "footwear",
"label": 67284
}
]
},
{
"country": "Fr",
"shares": [{
"productdivision": "accessorie",
"label": 5862
},
{
"productdivision": "apparel",
"label": 45312
},
{
"productdivision": "footwear",
"label": 26284
}
]
}
]
};
var root = d3.hierarchy(data, function(d) {
return d.shares;
})
.sum(function(d) {
return d.label
});
console.log(root)
<script src="https://d3js.org/d3.v4.min.js"></script>
I'm a newbie for Meteor.js but however I need to make some chart like http://nvd3.org/examples/pie.html. Bu I have no idea to render data on my html page.
Pie = new Meteor.Collection("pie");
if (Meteor.isClient) {
Template.chart.created = function(){ //not sure for template style.
_.defer(function () {
Deps.autorun(function () {
pp = Pie.find({}, {fields: {_id: 0}});
exampleData = _.toArray(pp);
console.log(exampleData);
//Regular pie chart example
nv.addGraph(function() {
var chart = nv.models.pieChart()
.x(function(d) { return d.label })
.y(function(d) { return d.value })
.showLabels(true);
d3.select("#chart svg")
.datum(exampleData)
.transition().duration(350)
.call(chart);
return chart;
});
//Donut chart example
nv.addGraph(function() {
var chart = nv.models.pieChart()
.x(function(d) { return d.label })
.y(function(d) { return d.value })
.showLabels(true) //Display pie labels
.labelThreshold(.05) //Configure the minimum slice size for labels to show up
.labelType("percent") //Configure what type of data to show in the label. Can be "key", "value" or "percent"
.donut(true) //Turn on Donut mode. Makes pie chart look tasty!
.donutRatio(0.35) //Configure how big you want the donut hole size to be.
;
d3.select("#chart2 svg")
.datum(exampleData)
.transition().duration(350)
.call(chart);
return chart;
});
});
});
}
}
if (Meteor.isServer) {
Meteor.startup(function () {
if(Pie.find().count() === 0) {
var data = [
{
"label": "One",
"value" : 29.765957771107
} ,
{
"label": "Two",
"value" : 0
} ,
{
"label": "Three",
"value" : 32.807804682612
} ,
{
"label": "Four",
"value" : 196.45946739256
} ,
{
"label": "Five",
"value" : 0.19434030906893
} ,
{
"label": "Six",
"value" : 98.079782601442
} ,
{
"label": "Seven",
"value" : 13.925743130903
} ,
{
"label": "Eight",
"value" : 5.1387322875705
}
];
for(var i=0; i < data.length; i++){
Pie.insert({
label: data[i].label,
value: data[i].value
});
}
}
});
}
d3.html
<head>
<meta charset="utf-8">
<title>d3</title>
</head>
<body>
{{> chart}}
</body>
<template name="chart">
<div width="500" height="500">
<svg id="chart"></svg>
<svg id="chart2"></svg>
</div>
</template>
As tested with Meteor both prior to Blaze and with Blaze: it is enough to start your Deps.autorun in the rendered callback of the template and just put your d3 drawing code there.
In Meteor prior to v.0.8.0, you would need to wrap the svg part into #constant region but once you use Blaze, it is not necessary.
An alternative is to draw everything once in rendered callback and then start observeChanges and keep your d3 view-model up to date.
I have a simple example here: https://github.com/Slava/d3-meteor-basic
I am working with JSON like the following:
[
{
"source": "Google",
"date": "2014-02-01",
"spend": 21,
"clicks": 1000
},
{
"source": "Bing",
"date": "2014-02-01",
"spend": 5,
"clicks": 541
},
{
"source": "Google",
"date": "2014-02-02",
"spend": 24,
"clicks": 1029
},
{
"source": "Bing",
"date": "2014-02-02",
"spend": 12,
"clicks": 754
}
]
And want to feed it into Crossfilter to create a line chart with NVD3. I don't know where to start, but I want the line chart to use the following:
X Axis - Date
Y Axis - Clicks
1 line per source
This is what I've been able to build without Crossfilter:
(function () {
var ymdFormat = d3.time.format('%Y-%m-%d');
d3.json('./data.json', function (err, json) {
nv.addGraph(function () {
var chart = nv.models.lineChart()
chart.margin({ left: 100 })
.useInteractiveGuideline(true)
.transitionDuration(350)
.showLegend(true)
.showYAxis(true).showXAxis(true);
chart.xAxis
.axisLabel('Date')
.tickFormat(function (d) {
return d3.time.format('%b %Y')(new Date(d));
});
chart.yAxis
.axisLabel('Clicks')
.tickFormat(d3.format(','));
data = parseData(json);
d3.select('#graph').append('svg')
.datum(data).call(chart);
});
})
function parseData(json) {
var data, result, key;
data = {};
json.forEach(function (elmt) {
if (!(elmt.source in data)) {
data[elmt.source] = { values: [] }
}
data[elmt.source].values.push({ x: ymdFormat.parse(elmt.date), y: elmt.clicks })
});
result = [];
for (key in data) {
if (data.hasOwnProperty(key)) {
result.push({
key: key,
values: data[key].values
});
}
}
console.log(result);
return result;
}
})()
I have this problem. I have a solution for this, not very nice, however.
var filterByMonthSource = filter.dimension(function(d) { return d.date + ':' + d.source });
var clicksGroup = filterByMonth.group().reduceSum(function(d) { return d.clicks });
var clicksData = clicksGroup.all();
clicksData will have [{key: "2014-02-01:Google", value: ...}, ...]
I then have to break clicksData into array of 3 fields.
I'm trying to convert my csv to the format needed by nvd3's stacked area chart: http://nvd3.org/ghpages/stackedArea.html
but got lost in the arrays conversion. Can someone help?
csv:
length,m1,m2,m3,m4
9,1,2,3,4
99,11,22,33,44
999,111,222,333,444
format needed by nvd3
var histcatexplong = [
{
"key" : "Consumer Discretionary" ,
"values" : [ [ 0000000000000 , 27.38478809681] , [ 0000000000000 , 27.371377218208] , [ 0000000000000 , 26.823411519395] } ,
{
"key" : "Consumer Staples" ,
"values" : [ [ 0000000000000 , 27.45458809681] , [ 0000000000000 , 27.444444444408] , [ 0000000000000 , 26.455555555395] } ,
so if the conversion is right, I should get:
var myall = [
{
"key" : "m3" ,
"values" : [ [ 9 , 3] , [ 99, 33] , [ 999, 333] } ,
{
"key" : "m1" ,
"values" : [ [ 9 , 1] , [ 99, 11] , [ 999, 111] } ,
My code for the conversion:
d3.csv("s1.csv", function (csv) {
var myall = [
{
"key" : "m3",
"values" : []
},
{
"key" : "m2",
"values" : []
}
];
v3 = csv.map(function(d) { return [ +d["length"], +d["m3"] ]; });
v2 = csv.map(function(d) { return [ +d["length"], +d["m2"] ]; });
d3.keys(csv).forEach(function(d) {
myall[0].values.push(v3);
myall[1].values.push(v2);
});
console.log(myall);
The problem is that myall didn't show up in the DOM (console output seems to be missing a top hierarchy:
[Object { key="m345", values=[249]}, Object { key="m2", values=[249]}]
For the nvd3 stacked area chart example, DOM copy/paste for the histcatexplong var:
*histcatexplong
[Object { key="Consumer Discretionary", values=[77]}, Object { key="Consumer Staples", values=[77]}, Object { key="Energy", values=[77]}, 7 more...]*
Thanks.
After some debugging, I fixed the issue. As a help to fellow learners, I post the code.
This is useful for people that need:
a. nvd3 stacked area chart(gives you the tooltips and other utilities for free i.e. no extra programming)
b. x-axis with values instead of dates
c. has csv data (flat hierarchy) in this format:
length,m1,m2
103.10,111,2222
137.91,0.36980639547531,99.6301936045247
113.30,0.176522506619594,99.8234774933804
159.59,0.244376214048499,99.7556237859515
code (modified from http://nvd3.org/ghpages/stackedArea.html):
<script>
<!DOCTYPE html>
<meta charset="utf-8">
<link href="../src/nv.d3.css" rel="stylesheet" type="text/css">
<style>
body {
overflow-y:scroll;
}
text {
font: 12px sans-serif;
}
#chart1, #chart2 {
height: 500px;
}
</style>
<body>
<div>
<svg id="chart1"></svg>
</div>
<script src="../lib/d3.v3.js"></script>
<script src="../nv.d3.js"></script>
<script src="../src/utils.js"></script>
<script src="../src/models/axis.js"></script>
<script src="../src/tooltip.js"></script>
<script src="../src/models/legend.js"></script>
<script src="../src/models/axis.js"></script>
<script src="../src/models/scatter.js"></script>
<script src="../src/models/stackedArea.js"></script>
<script src="../src/models/stackedAreaChart.js"></script>
<script>
var myall = [
{
"key" : "m1",
"values" : []
},
{
"key" : "m2",
"values" : []
}
];
d3.csv("s1.csv", function (error, csv) {
if (error) return console.log("there was an error loading the csv: " + error);
console.log("there are " + csv.length + " elements in my csv set");
csv.sort(function(a,b) {return a.length-b.length;});
var mmm = ["m1","m2"];
for (var i = 0; i < mmm.length; i++) {
myall[i].values = csv.map(function(d) { return [ +d["length"], +d[mmm[i]] ]; });
};
var colors = d3.scale.category20();
keyColor = function(d, i) {return colors(d.key)};
var chart;
nv.addGraph(function() {
chart = nv.models.stackedAreaChart()
.x(function(d) { return d[0] })
.y(function(d) { return d[1] })
.color(keyColor)
.clipEdge(true);
// chart.xAxis
// .tickFormat(function(d) { return d3.time.format('%x')(new Date(d)) });
chart.yAxis
.tickFormat(d3.format(',.2f'));
d3.select('#chart1')
.datum(myall)
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
chart.dispatch.on('stateChange', function(e) { nv.log('New State:', JSON.stringify(e)); });
return chart;
});
// end read csv
});