I have data returned from a REST API in the following form.
[{
"created": "2014-06-01T11:21:47Z",
"is_good": false,
"amount": 10
},{
"created": "2014-06-01T12:01:00Z",
"is_good": false,
"amount": 12
},{
"created": "2014-06-02T10:00:00Z",
"is_good": true,
"amount": 8
},{
"created": "2014-06-02T08:00:00Z",
"is_good": false,
"amount": 3
},
...
]
In order to make a stacked bar chart, I thought the solution would be to use d3.nest() to rollup the amounts, first by date, then by is_good (the stacking category).
nestedData = d3.nest()
.key(function(d) { return d3.time.day(new Date(d.created)); })
.key(function(d) { return d.is_good; })
.rollup(function(leaves) { return {amount: d3.sum(leaves, function(d) { return d.amount; })}; })
.entries(jsonData);
That would probably be fine when drawing the chart following Mike Bostock's example here, but wouldn't work in a d3.layout.stack() call, because it requires the .values() to be the group iterable from which x and y is then calculated. That lead me to try the keys the other way around, but then drawing the chart itself becomes tricky.
So after all of that, I'm now wondering if there's a neat d3 way of flattening the nested values into something that resembles the datasets in almost all stacked bar chart examples.
Alternatively, perhaps I'm just not seeing how best to use the double nested data to create a stacked bar chart based on the examples.
Any help would be much appreciated.
I eventually decided to tackle this without using d3.layout.stack at all, and attempted to convert the double nested array into something resembling the example given. This is the code that will take that complex array and squash it down into something more manageable when drawing the chart.
data.forEach(function(d) {
var y0 = 0;
d.amounts = color.domain().map(function(is_good) {
return {is_good: is_good, y0: y0, y1: y0 += +d.values.filter(function(d) {
return d.key == is_good;
})[0].values.amount};
});
d.total = d.amounts[d.amounts.length - 1].y1;
});
Here's a working example.
I'm sure this isn't perfect, so if anyone has a better way of achieving the same result, I'd be interested to see it!
Related
I am new to dc.js and facing issues in deciding dimensions and groups. I have data like this
this.data = [
{Type:'Type1', Day:1, Count: 20},
{Type:'Type2', Day:1, Count: 10},
{Type:'Type1', Day:2, Count: 30},
{Type:'Type2', Day:2, Count: 10}
]
I have to show a composite chart of two linecharts one for type Type1 and other for Type2. My x-axis will be Day. So one of my dimensions will be Day
var ndx = crossfilter(this.data);
var dayDim = ndx.dimension(function(d) { return d.Day; })
How the grouping will be done? If I do it on Count, the total count of a particular Day shows up which I don't want.
Your question isn't entirely clear, but it sounds like you want to group by both Type and Day
One way to do it is to use composite keys:
var typeDayDimension = ndx.dimension(function(d) {return [d.Type, d.Day]; }),
typeDayGroup = typeDayDimension.group().reduceSum(function(d) { return d.Count; });
Then you could use the series chart to generate two line charts inside a composite chart.
var chart = dc.seriesChart("#test");
chart
.width(768)
.height(480)
.chart(function(c) { return dc.lineChart(c); })
// ...
.dimension(typeDayDimension)
.group(typeDayGroup)
.seriesAccessor(function(d) {return d.key[0];})
.keyAccessor(function(d) {return +d.key[1];}) // convert to number
// ...
See the series chart example for more details.
Although what Gordon suggested is working perfectly fine, if you want to achieve the same result using composite chart then you can use group.reduce(add, remove, initial) method.
function reduceAdd(p, v) {
if (v.Type === "Type1") {
p.docCount += v.Count;
}
return p;
}
function reduceRemove(p, v) {
if (v.Type === "Type1") {
p.docCount -= v.Count;
}
return p;
}
function reduceInitial() {
return { docCount: 0 };
}
Here's an example: http://jsfiddle.net/curtisp/7frw79q6
Quoting Gordon:
Series chart is just a composite chart with the automatic splitting of the data and generation of the child charts.
I've been trying to get a telerik kendo ui column chart to display grouped data but where the groups might not have entries for all possible values and I don't want to show space/empty columns in these empty cases.
Telerik dojo of problem
Is anyone aware of anyway to get this to work more like the screenshot below
Excel has grouped the data but doesn't display a column at all if the data is null/zero
I couldn't find a built-in way to do this, so I ended up just placing the bars manually by overriding the visual function. In my case, I only needed to move one bar and that bar will always be the same category, which made it a whole lot easier in that I only had to identify it by matching the category. I could then move it with a transform. You cannot move it by setting the coordinates because the visual has already been created.
It would be more complex to do this dynamically, but it's certainly possible. This may give someone a start in the right direction.
One downside of this method is that you must also place the labels manually, which I have also done below. You can override the visual function of the labels, as well, but no references to any other elements are passed with the event data. Note how the documentation says the sender field may be undefined; in my experience, this is always the case.
It also does not move the tooltip or the highlight. You could use the same method to move the highlight (override the visual function, though on the series instead of the seriesDefaults) and draw the tooltip manually while moving the highlight -- similar to how the method below draws the label while moving the column.
Telerik Dojo Example
$(document).ready(function () {
$("#chart").kendoChart({
legend: { visible: false },
tooltip: { visible: false },
categoryAxis: {
name: "categoryAxis",
categories: ["1", "2", "3"],
},
series: [
{
data: [1, 2, 3],
highlight: { visible: false },
},
{
data: [1.5, null, 3.5],
highlight: { visible: false },
}
],
seriesDefaults: {
type: "column",
labels: { visible: false },
visual: function (e) {
if (e.value === null) return;
var visual = e.createVisual();
var axisRect = e.sender.getAxis("categoryAxis").slot("2");
var group = new kendo.drawing.Group();
var label = new kendo.drawing.Text(e.value, [0, 0], {
font: "20px sans-serif",
fill: { color: "black" }
});
var lbox = label.clippedBBox();
label.position([
e.rect.origin.x + e.rect.size.width / 2 - lbox.size.width / 2,
e.rect.origin.y - label.bbox().size.height * 1.5
]);
group.append(visual, label);
if (e.category === "2") {
var x = (axisRect.origin.x + axisRect.size.width / 2) - e.rect.size.width / 2;
group.transform(kendo.geometry.transform().translate(x - e.rect.origin.x, 0));
}
return group;
},
}
});
});
So for some reason I have to put a tiny chart into a column cell.
I did it with a simple div and initizialized every single chart:
<div>
<canvas id="6" height="50px" width="150px"></canvas>
</div>
jsFiddle
Which works great for 10 dataset or 20. But i got a huge datatable with 380 rows and after "2 minutes" of waiting it even loads the charts for every single table row.
Is there a better way to do it, or boost performance?
I found the solution.
Like mentioned in this post: Pagination triggers
I call a function which inits the visible charts on datatable draw. Means i only draw them when they get viewed. Nice performance and super slim solution.
$('#Table')
.on( 'draw.dt', function () { initSparkline(); } )
.dataTable();
});
Another option might be to use the drawCallback option to use data in the table to render charts for the visible rows each time the DataTable is re-drawn for a page change.
Below is an simplified example of how this might be done for a doughnut chart.
$('#dataTableId').DataTable({
// I use the "columns" option to tell each column what data to show.
// One column should have a uniquely id'ed canvas.
"columns": [
{ "data": "id" },
{ "data": "A" },
{ "data": "B" },
{ "data": function(row){
return "<canvas height=\"40px\" width=\"40px\" id=\"chart"+row.id+"\"></canvas>";
},
"orderable": false}
],
"drawCallback": function() {
// Get data, only from the rows displayed on the current page.
var data = this.api().rows({page:'current'}).data();
// The first draw appears to have a length of 0,
// but subsequent ones have length equal to number of rows drawn.
if (data.length !== 0){
// Loop through each row to render each chart
for (var i = 0; i < data.length; i++) {
// Find the chart intended for this data
var ctx = $("#chart"+data[i].id);
// Make the chart
var newChart = new Chart(ctx, {
"type": "doughnut",
"data": {
"labels": [
"A",
"B"
],
"datasets": [{
"data": [data[i].A,data[i].B]
}],
},
}) // /Chart
}
}
}
// In a real table, the object passed into the DataTable() function will
// probably also use other options, such as "ajax", "serverSide" or
// "pageLength". For simplicity, only "columns" and "drawCallback" are
// shown in this example.
}); // /DataTable
I'm learning about nesting and have been looking at phoebe bright's explanations, where she writes:
var nested_data = d3.nest()
.key(function(d) { return d.status; })
.entries(csv_data);
gets this:
[
{
"key": "Complete",
"values": [
{
"id": "T-024",
"name": "Organisation list in directory",
"priority": "MUST",
},
{
When I try to do the same, in my console, if I can recreate it, looks like this:
Object
key: "1847"
values: Array [2]
0: Object
production: "A Mirror for Witches"
1: Object
production: "Sadlers Wells"
When I try to display the "Values" as text, all I get is [Object, object] in my html, where what I want is the production names.
How do I do this? I have tried nesting production as a key also, but this doesn't seem to work, and have also tried returning the index when returning the values, but can't get that to work either.
Any help I will really appreciate, thanks.
Here is my code
data.csv
year,production,company
1847,A Mirror for Witches
1847,Sadlers Wells
d3.csv("data.csv", function(csv_data){
var nested_data = d3.nest()
.key(function(d) { return d.year; })
.entries(csv_data)
console.log(nested_data);
var selection =
d3.select("body").selectAll("div")
.data(nested_data)
.enter()
selection.append("div")
.classed('classed', true)
.text(function(d){
return d.key;
});
d3.selectAll(".classed").append("div")
.text(function(d){
return d.values;
});
});
Here's a working plunk: http://plnkr.co/edit/0QuH8P9ujMdl0vWuQkrQ?p=preview
I added a few more lines of data to show it working properly.
The thing to do here is to add a second selection (I've called it production_selection) and bind data based off the first selection (year_selection): You use nested selections to show nested data.
First selection (show a div for each year, or key, in your nested data):
var year_selection = d3.select("#chart").selectAll("div")
.data(nested_data)
.enter().append("div")
...
Second selection (show all productions, or values, under that key):
var production_selection = year_selection.selectAll(".classed")
.data(function(d) { return d.values; })
.enter().append("div")
...
For the second selection, you just define the accessor function (d.values)
I have the following complex data structure:
[
Object {id: 15, targets: Array[2]},
Object {id: 26, targets: Array[2]},
Object {id: 39, targets: Array[2]}
]
'targets' is an array of objects. Each of them has this shape:
Object {idTarget: "target1", events: Array[315]}
Object {idTarget: "target2", events: Array[310]}
'events' is an array with the real values to plot.
So, each element has this shape:
Object {timestamp: 1373241642, value: 1801.335}
Now, with this structured dataset, I would like to create an svg group 'g' for each external object (I am referring to 15, 26 and 39) and inside each group I want to create two lines, one for each target, using the values in 'events'.
Having a flat dataset it's easy to proceed in the drawing following the pattern: select + data + enter + append, but I am having trouble with this complex dataset.
For example I don't even know how to assign a key function to start.
svg.selectAll('.element')
.data(data, function(d){ return d.id + ?})
I would like to have this kind of keys '15target1', '15target2', '26target1', '26target2' and so on.
Do you recommend to simplify the dataset giving up the possibility of having neat groups or there is a workaround here that lets me easily draw what I want?
Thanks.
You want nested selections for this. Your code would look something like this.
var gs = d3.selectAll("g").data(data, function(d) { return d.id; });
gs.enter().append("g");
var line = d3.svg.line().x(function(d) { return xscale(d.timestamp); })
.y(function(d) { return yscale(d.value); });
gs.selectAll("path")
.data(function(d) { return d.targets; }, function(d) { return d.idTarget; })
.enter().append("path")
.attr("d", function(d) { return line(d.events); });