I want to create a "Timeline" type rowchart using dc.js. I am able to produce this chart in d3, but cannot replicate in dc.
The rowchart will have have a timeline x-axis and the bars will start at a point in time on the axis and end at another point in time on the axis.
The D3 version looks like this
Each row of data has an ID, a startDate and an endDate:
{
"id": 31,
"startDate": "2016-9-22",
"endDate": "2019-1-15"
},
{
"id": 29,
"startDate": "2016-9-21",
"endDate": "2016-9-28"
}
To achieve this in D3 I set up a scale:
let xScale = d3.scaleTime()
.domain([d3.min(data, function(d){ return new Date(d.startDate)}), d3.max(data, function(d){ return new Date(d.endDate)})])
.range([0, 500]);
Then I append rects, setting the x and width attributes using the xScale:
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d){ return xScale(new Date(d.startDate))})
.attr('width', function(d) { return xScale(new Date(d.startDate)) - xScale(new Date(d.endDate))})
In dc.js I have gotten part way, but there is some behaviour that I do not understand
I define the crossfilter, dimension and xScale:
let ndx = crossfilter(data);
let idDim = ndx.dimension(function(d){ return d.id});
let xScale = d3.scaleTime()
.domain([d3.min(data, function(d){ return new Date(d.dateCreate)}), d3.max(data, function(d){ return new Date(d.dateCompleted)})])
.range([0, 500])
Then I create a group:
let idGroup = idDim.group().reduceSum(function(d){ return new Date(d.dateCompleted)});
(For this example I am only trying to get the width of the bar to extend from the 0 point to the dateCompleted, i will worry about setting the x attribute later)
And I create the chart like this:
idChart
.dimension(idDim)
.group(idGroup)
.height(500)
.width(500)
.x(xScale)
dc.renderAll()
The result is that the bars appear, and are roughly in the right spot, except that they are much longer than they should be and are translated by -9746 pixels.
dc.js version looks like this
I assume that I am not parsing dates correctly, does anyone have any ideas where I am going wrong? How do I stop this translation? Like I said, I think that once I have this working, setting the x attribute should be easy.
I found out that "cleaning" and normalising the data before feeding it to crossfilter works better, makes it easier to understand and way faster.
eg instead of doing new Date(d.xxx) everytime you need the date, start your code with looping through each of your data and transform it once for all
Also, I use d3 to transform the date as string to js Date
var day = d3.time.format("%Y-%m-%d");
data.forEach(function(d){
{
d.id = +d.id; //convert the id to an int, json is string only
d.startDate = day(d.startDate)
d.endDate = day(d.endDate)
})
Related
I want to draw a pie chart for every point on the map instead of a circle.
The map and the points are displaying well but the pie chart is not showing over the map points. There is no error also. I can see the added pie chart code inside map also.
Below is the code snippet .
var w = 600;
var h = 600;
var bounds = [[78,30], [87, 8]]; // rough extents of India
var proj = d3.geo.mercator()
.scale(800)
.translate([w/2,h/2])
.rotate([(bounds[0][0] + bounds[1][0]) / -2,
(bounds[0][1] + bounds[1][1]) / -2]); // rotate the project to bring India into view.
var path = d3.geo.path().projection(proj);
var map = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
var india = map.append("svg:g")
.attr("id", "india");
var gDataPoints = map.append("g"); // appended second
d3.json("data/states.json", function(json) {
india.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("data/water.csv", function(csv) {
console.log(JSON.stringify(csv))
gDataPoints.selectAll("circle")
.data(csv)
.enter()
.append("circle")
.attr("id", function (d,i) {
return "chart"+i;
})
.attr("cx", function (d) {
return proj([d.lon, d.lat])[0];
})
.attr("cy", function (d) {
return proj([d.lon, d.lat])[1];
})
.attr("r", function (d) {
return 3;
})
.each(function (d,i) {
barchart("chart"+i);
})
.style("fill", "red")
//.style("opacity", 1);
});
function barchart(id){
var data=[15,30,35,20];
var radius=30;
var color=d3.scale.category10()
var svg1=d3.select("#"+id)
.append("svg").attr('width',100).attr('height',100);
var group=svg1.append('g').attr("transform","translate(" + radius + "," + radius + ")");
var arc=d3.svg.arc()
.innerRadius('0')
.outerRadius(radius);
var pie=d3.layout.pie()
.value(function(d){
return d;
});
var arcs=group.selectAll(".arc")
.data(pie(data))
.enter()
.append('g')
.attr('class','arc')
arcs.append('path')
.attr('d',arc)
.attr("fill",function(d,i){
return color(d.data);
//return colors[i]
});
}
water.csv:
lon,lat,quality,complaints
80.06,20.07,4,17
72.822,18.968,2,62
77.216,28.613,5,49
92.79,87.208,4,3
87.208,21.813,1,12
77.589,12.987,2,54
16.320,75.724,4,7
In testing your code I was unable to see the pie charts rendering, at all. But, I believe I still have a solution for you.
You do not need a separate pie chart function to call on each point. I'm sure that there are a diversity of opinions on this, but d3 questions on Stack Overflow often invoke extra functions that lengthen code while under-utilizing d3's strengths and built in functionality.
Why do I feel this way in this case? It is hard to preserve the link between data bound to svg objects and your pie chart function, which is why you have to pass the id of the point to your function. This will be compounded if you want to have pie chart data in your csv itself.
With d3's databinding and selections, you can do everything you need with much simpler code. It took me some time to get the hang of how to do this, but it does make life easier once you get the hang of it.
Note: I apologize, I ported the code you've posted to d3v4, but I've included a link to the d3v3 code below, as well as d3v4, though in the snippets the only apparent change may be from color(i) to color[i]
In this case, rather than calling a function to append pie charts to each circle element with selection.each(), we can append a g element instead and then append elements directly to each g with selections.
Also, to make life easier, if we initially append each g element with a transform, we can use relative measurements to place items in each g, rather than finding out the absolute svg coordinates we would need otherwise.
d3.csv("water.csv", function(error, water) {
// Append one g element for each row in the csv and bind data to it:
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("id", function (d,i) { return "chart"+i; })
.append("g").attr("class","pies");
// Add a circle to it if needed
points.append("circle")
.attr("r", 3)
.style("fill", "red");
// Select each g element we created, and fill it with pie chart:
var pies = points.selectAll(".pies")
.data(pie([0,15,30,35,20]))
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
Now, what if we wanted to show data from the csv for each pie chart, and perhaps add a label. This is now done quite easily. In the csv, if there was a column labelled data, with values separated by a dash, and a column named label, we could easily adjust our code to show this new data:
d3.csv("water.csv", function(error, water) {
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("class","pies")
points.append("text")
.attr("y", -radius - 5)
.text(function(d) { return d.label })
.style('text-anchor','middle');
var pies = points.selectAll(".pies")
.data(function(d) { return pie(d.data.split(['-'])); })
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
The data we want to display is already bound to the initial g that we created for each row in the csv. Now all we have to do is append the elements we want to display and choose what properties of the bound data we want to show.
The result in this case looks like:
I've posted examples in v3 and v4 to show a potential implementation that follows the above approach for the pie charts:
With one static data array for all pie charts as in the example: v4 and v3
And by pulling data from the csv to display: v4 and v3
IN d3 chart, I don't want to use Date in x-axis. I have simple category and count of that category value and I want to create a chart using these value.
I am using the below URL to create the line chart
http://bl.ocks.org/mbostock/3883245
But in place of year, I want to show the category name in x-axis.
You want to use an ordinal scale instead of a time-scale for the x-axis, have a look at this.
Basically replace
var x = d3.scaleTime()
.rangeRound([0, width]);
...
x.domain(d3.extent(data, function(d) { return d.date; }));
with
var x = d3.scaleBand()
.rangeRound([0, width]);
...
x.domain(data.map(function(d) { return d.date; }));
But it is hard to answer your question without any code examples of what you have tried so far and what you hide. If you provide for example a jsfiddle of your code you will get much better answers and not a question that gets downvoted.
I am trying to use d3 to construct an array of pie charts that vary in size based on the total value of the data in the pie chart.
Does anyone know how i could take the html file from the d3 example:
http://bl.ocks.org/mbostock/3888852
And modify the pie charts radii to be proportional to the total of the data from the csv file for that pie chart?
Here's a quick attempt:
...
data.forEach(function(d) {
d.total = 0;
d.ages = color.domain().map(function(name) {
d.total += +d[name]; // add a total of all population for each state
return {name: name, population: +d[name]};
});
});
// determine the maximum population for all states (ie California)
var maxPopulation = d3.max(data, function(d){
return d.total;
});
// now for each d.age (where the the arcs come from)
// stash an arc function based on a scaled size
data.forEach(function(d) {
var r = radius * (d.total / maxPopulation);
var someArc = d3.svg.arc() // make the radius scale according to our max population
.outerRadius(r)
.innerRadius(r - (r-10));
d.ages.forEach(function(a){
a.arc = someArc; // stash the arc in the d.ages data so we can access later
})
});
...
svg.selectAll(".arc")
.data(function(d) { return pie(d.ages); })
.enter().append("path")
.attr("class", "arc")
.attr("d", function(d){
return d.data.arc(d); // use our stashed arc function to create the path arc
})
.style("fill", function(d) { return color(d.data.name); })
Example here.
You might have to change out how I scale the size of the pie. My linear approach works but some state have such few people compared to CA that their pie charts are view-able.
I'm trying to highlight some points in a time series modeled using a nvd3.js eventLineChart - more precisely I have a json object with time-stamps and for each time-stamp I would like to add a vertical line at this particular date/time. The highlighted points may not exist in the time-series data source and are global over all groups of the time-series data (like ticks).
Any ideas on how this could be achieved? - I tried adding a standard line to my plot (fixed y1 and y2 and x according to the timestamp of the events i want to highlight) but wasn't able to have the timestamps scaled to the same range as the original time series.
Here are some parts of the model I started to build for that purpose based on nv.models.lineChart. - (just an excerpt of the model as most of the code is just a copy from the lineChart model):
nv.models.eventLineChart = function() {
// adds vertical highlights to line chart
"use strict";
var chartEvents = {}
function chart(selection) {
selection.each(function(data) {
// Setup Scales
x = lines.xScale();
y = lines.yScale();
// Setup containers and skeleton of chart
var gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-lineChart').append('g');
var g = wrap.select('g');
gEnter.append('g').attr('class', 'nv-eventLinesWrap');
//------------------------------------------------------------
// Main Chart Component(s)
var eventWrap = wrap
.select('.nv-eventLinesWrap')
.selectAll('.nv-eventLines')
.data(function(d) {return d });
eventWrap
.enter()
.append('g')
.attr('class', 'nv-eventLines');
// chartEvents json ==> [{decsription: "test,"timestamp: 1375031820000}]
var eventLine = eventWrap
.selectAll('.nv-eventLine')
.data(chartEvents, function(d){return (d.timestamp)});
var eventLineEnter = eventLine.enter()
.append('line').attr('class', 'nv-eventLine')
.style('stroke-opacity', 0);
// #todo set ymin and ymax
eventLine
.attr('x1', function(d){ return x(d.timestamp);})
.attr('x2', function(d){ return x(d.timestamp);})
.attr('y1', 300)
.attr('y2', 800)
.style('stroke', function(d,i,j) { return color(d,j) })
.style('stroke-opacity', function(d,i) {
return 1;
});
});
return chart;
}
chart.setChartEvents = function(_) {
if (!arguments.length) return chartEvents;
chartEvents = _;
return chart;
};
return chart;}
This model is called by using:
nv.addGraph(function() {
var nv3dChart = nv.models.eventLineChart().useInteractiveGuideline(true).setChartEvents(json.chartEvents);
// json.chartEvents >> [{decsription: "EventDescription,"timestamp: 1375031820000}]
nv3dChart.xAxis
.showMaxMin(false);
.tickFormat(function(d) { return d3.time.format("%Y-%m-%d")(new Date(d)) });
nv3dChart.yAxis
.axisLabel(widgetConfig.action.data.kpiName)
.tickFormat(d3.format(',.f'));
var ndg = d3.select(renderToElementId+' svg');
ndg.datum([{
values: json.data,
key: widgetConfig.action.data.tagName
}])
.transition().duration(500);
nv.utils.windowResize(nv3dChart.update);
return nv3dChart;})
Which produces currently this svg output (events that should be displayed by vertical lines only)
<g class="nv-eventLines">
<line class="nv-eventLine" x1="1375031820000" x2="1375031820000" y1="300" y2="800" style="stroke: #1f77b4;"></line>
</g>
.. as described I haven't yet figured out a way to implement the scaling of the events x values according to the scale of the line chart
Would greatly appreciate any help regarding this problem
I now manually created all scales for x and y and added them to the nvd3 elements. I'm not particularly happy with that solution as it prevents me from creating a more modular feature for multiple nvd3 charts but it is a starting point.
Here is an outline of my current solution:
nv.models.eventLineChart = function() {
// initialize scales
var y = d3.scale.linear(),
x = d3.scale.linear();
// set scales of lines
lines = nv.models.line()
.yScale(y)
function chart(selection) {
//#todo support for multiple series
// set domain and range for scales
x
.domain(d3.extent(data[0].values, function(d){return d.x}))
.range([0,availableWidth]);
y
.domain(d3.extent(data[0].values, function(d){return d.y}))
.range([0,availableHeight]);
// add container for vertical lines
gEnter.append('g').attr('class', 'nv-eventLinesWrap');
// setup container
var eventWrap = wrap.select('.nv-eventLinesWrap').selectAll('.nv-eventLines')
.data(function(d) {return d });
eventWrap.enter().append('g').attr('class', 'nv-eventLines');
eventWrap.select('.nv-eventLinesWrap').attr('transform', 'translate(0,' + (-margin.top) +')');
var eventLine = eventWrap.selectAll('.nv-eventLine').data(chartEvents, function(d){return (d.timestamp)});
var eventLineEnter = eventLine.enter()
.append('line').attr('class', 'nv-eventLine')
// configure and style vertical lines
//#todo: set color for lines
eventLine
.attr('x1', function(d){ return x(d.timestamp)})
.attr('x2', function(d){ return x(d.timestamp)})
.attr('y1', y.range()[0])
.attr('y2', y.range()[1])
.style('stroke', function(d,i,j) { return "#000"; })
.style('stroke-width',1)
// #todo add vertical lines via transitions, add vLine content to toolbox
}}
Thank you, Lars, for your contributions .. they really helped a lot to understand certain parts in more detail.
If anyone has come up with a better idea to solve this problem I would be very grateful if you could post these suggestions here!
I'm using d3 to build a scatter plot and the set of values I have for the x and y axes both have positive and negative values. This is my first attempt at using d3 and I've gathered from introductory tutorials such as On the tenth day of Xmas, get dirty with data using d3.js, that the key is setting up the x and y scales correctly.
Next, I found this tutorial: Bar Chart with Negative Values, which has almost helped me get it right (I think). My data set is too hefty to place here, but here is the code I have a sample of my data to work with:
<div id="compareAll"></div>
<script>
window.onload = function() {
var dataSet = [ [4,4], [-4,4], [-4,-4], [4,-4], ];
var x0 = Math.max(-d3.min(dataSet[0]), d3.max(dataSet[0]));
var xScale = d3.scale.ordinal()
.domain([-x0,x0])
.range([0,10]);
var yScale = d3.scale.ordinal()
.domain(d3.range(dataSet[1])
.rangeRoundBands([0,10]);
var svg = d3.select("#compareAll")
.append("svg")
.attr("width", 10)
.attr("height",10)
svg.selectAll("circle")
.data(dataSet)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r",4);
var xAxis = d3.svg.axis()
.scale(xScale)
.tickSize(1);
var yAxis = d3.svg.axis()
.scale(yScale)
.tickSize(1);
svg.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0,10)")
.call(xAxis);
svg.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(10,0)")
.call(yAxis);
}
</script>
Frankly, I don't understand the use of the ordinal scale in the Bar Chart example, however I got seemly worse results when I took it out and made the yScale with the same syntax as the xScale.
Any explanation of the best way to go about working with positive and negative values for both axes would be appreciated. Let me know if I'm missing something obvious. I through together this example from a much larger project as to make it as specific and easy to read as possible.
Thanks!
There's nothing special about using negative values for your axes - you should simply be using linear scales for your axes, and setting the domain to [min, max], where min might well be negative.
See a working version of your code here: http://jsfiddle.net/nrabinowitz/qN5Sa/
A few notes on this:
You should definitely use linear scales for a scatterplot, unless you want more of a matrix. A standard x-scale might look like this:
var xScale = d3.scale.linear()
.domain([-5, 5])
.range([0,w]);
The d3.svg.axis() component includes an .orient() setting - by default, this is "bottom", i.e. a horizontal axis with numbers below the line. To make a correctly oriented y-axis, use .orient('left').
I didn't include the code to determine the min/max domain for each axis based on your data, because I figured it would complicate the example. But you can do this fairly easily if you don't know the bounds of your data:
// assuming data like [[x0,y0], [x1,y1]]
var xmin = d3.min(data, function(d) { return d[0] }),
xmax = d3.max(data, function(d) { return d[0] }),
ymin = d3.min(data, function(d) { return d[1] }),
ymax = d3.max(data, function(d) { return d[1] });