Modify Donut Chart Slices in C3/D3 - javascript

I have created a simple donut chart using c3.js. Here is the FIDDLE.
If you hover over a slice of the Donut it will stick out. I was wondering if it is possible for the slice to stick out by default without hovering over.
For example i want slice A, slice B, and C to stickout by default How can I do that?
Here is my Code
var currentSlice;
var chart = c3.generate({
data: {
x: 'x',
columns: [
['x', '2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04', '2013-01-05', '2013-01-06'],
['A', 30, 200, 100, 400, 150, 250],
['B', 130, 100, 140, 200, 150, 50],
['C', 50, 100, 130, 240, 200, 150],
['D', 130, 100, 140, 200, 150, 50],
['E', 130, 150, 200, 300, 200, 100]
],
type: 'donut',
onclick: function (e) {
},
onmouseover: function (d, i) {
if(currentSlice !== void 0) {
'currentSlice'.attr("transform","scale(1)")
}
currentSlice = d3.select(i).attr("transform", "scale(1.1)");
},
onmouseout: function (d, i) {
}
},
axis: {
x: {
type: 'timeseries',
tick: {
format: '%Y-%m-%d',
centered: true,
position: 'inner-right'
}
}
},
bindto: '#dash',
bar: {
width: {
ratio: 0.5 // this makes bar width 50% of length between ticks
}
},
pie: {
expand: true,
},
tooltip: {
grouped: false,
contents: function (data, defaultTitleFormat, defaultValueFormat, color) {
// console.log("Containt");
// console.log(data, defaultTitleFormat, defaultValueFormat, color);
return "<p style='border:1px solid red;'>" + data[0].value + "</p>";
}
}
});

c3js has two options, but both require a slight hack with 'setTimeout' to force our default scaling to happen after rendering and animation occur.
The onrendered function is available to set within the c3config object that one initializes the chart with. This function is triggered after a redraw is triggered but before visual rendering happens in the DOM. However, there is a hack to use setTimeout since it will create a separate callstack that will execute after the current callstack which in c3 happens to include redrawing the graph. (explanation of setTimeout to force logic to run after current callstack executes)
The load function exposes a done callback that is triggered after the elements are rendered to the DOM but before the animation finishes. So if one sets the initial scale transform in done, if the animations triggered by load are using the scale transform (which loading a pie chart appears to do), then the last keyframe of the animation will overwrite your modified scale back to scale(1). However, we can similarly use setTimeout to run our code after the current callstack (which includes animation) executes.
In exploring this, I created a generalized form of rby's answer, and I offer two alternative paths to setting default scales through the onrendered and done functions exposed in c3. (Fiddle):
var selected = ['A', 'B', 'C'];
var _ARC = '.c3-arc';
var _SCALING = '1.1';
function getCurrentlySelected() {
var _PREFIX = _ARC + '-';
return d3.selectAll(_PREFIX + selected.join(', ' + _PREFIX));
}
Within c3config object and onrendered through initialization:
var chart = c3.generate({
bindto: '#chart',
data: { ... },
onrendered: function() {
setTimeout(function() {
if (selected.length > 0) {
getCurrentlySelected().attr('transform', 'scale(' + _SCALING + ')');
}
}); // Notice we don't need a delay, just taking advantage to force our logic to happen after current callstack is executed
}
});
Also possible to use load with done after initialization:
chart.load({
columns: [
['A', 30, 200, 100, 400, 150, 250],
['B', 130, 100, 140, 200, 150, 50],
['C', 50, 100, 130, 240, 200, 150],
['D', 130, 100, 140, 200, 150, 50],
['E', 130, 150, 200, 300, 200, 100]
],
done: function() {
setTimeout(function() {
if (selected.length > 0) {
getCurrentlySelected().attr('transform', 'scale(' + _SCALING + ')');
}
}) // Notice we don't need a delay, just taking advantage to force our logic to happen after current callstack is executed
}
});

You can use setTimeout() to scale specific slices, once the chart is rendered. Here's one way:
setTimeout( function() {
d3.selectAll('.c3-arc-A, .c3-arc-B, .c3-arc-C').attr("transform", "scale(1.2)");
}, 5);
Place this call after your c3.generate() call.

Related

Marker in BillboardJS on defocus

When hovering over a line in billboardjs you can see a marker which follows the mouse (a tall vertical line). Is there a function for putting a marker on the x-line which can be used without triggering an automatic marker via onmousemove/hovering over data-points?
var chart = bb.generate({
data: {
columns: [
["data1", 30, 200, 100, 400, 150, 250],
["data2", 50, 20, 10, 40, 15, 25]
],
type: "line", // for ESM specify as: line()
},
bindto: "#lineChart"
});
https://naver.github.io/billboard.js/demo/#Chart.LineChart
So to exemplify. I use an onclick (in the data object) in the chart which defocuses the view and I still want the marker to remain.
So the code would look something like:
var chart = bb.generate({
data: {
columns: [
["data1", 30, 200, 100, 400, 150, 250],
["data2", 50, 20, 10, 40, 15, 25]
],
type: "line", // for ESM specify as: line()
onclick: function (d) {
focusElsewhere()
showMarker(d.x)
}
},
bindto: "#lineChart"
});
So the question is if there is a function for this, or an obvious fix?
I have looked through https://naver.github.io/billboard.js/release/latest/doc/Chart.html but I may of course have missed something.
I found that using xgrids did the trick. I don't think that the documentation gives a good example of how to use it. But basically you can use the "value" field to give which point the line should be on and add a class to show different kinds of lines.
var chart = bb.generate({
data: {
columns: [
["data1", 30, 200, 100, 400, 150, 250],
["data2", 50, 20, 10, 40, 15, 25]
],
type: "line", // for ESM specify as: line()
onclick: function (d) {
focusElsewhere()
this.xgrids.add({ value: d.x, class: "hover-line" }); //showMarker(d.x)
}
},
bindto: "#lineChart"
});
To remove the line or reset the billboard for continued use so to say, you can use
xgrids․remove({}) and add an object with some parameters of what kind of lines you want to remove.

Custom colors in C3.js timeseries chart

I am trying to set custom colors in a c3.js timeseries chart following this example. The first element of each array is used to identify the dataset, so if I have an array:
var datatest1 = ['data1', 30, 200, 100, 400, 150, 250];
the color property can be accessed like this:
colors: {data1:'#0000'}
or:
colors: {'data1':'#0000'}
However, if I use the first element of the array to access them:
var data1id = datatest1[0];
and then:
colors: {data1id:'#0000'}
It fails. Not sure what I may doing wrong as I get no feedback in the browser...
Here is a working example:
var axis = ['x', '2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04', '2013-01-05', '2013-01-06'];
var datatest2 = ['data2', 130, 340, 200, 500, 250, 350];
var datatest1 = ['data1', 30, 200, 100, 400, 150, 250];
var data1id = datatest1[0];
var data2id = datatest2[0];
var chart = c3.generate({
data: {
x: 'x',
columns: [
axis,
datatest1,
datatest2
],
colors: {
//data1: '#0000',
//data2: '#0000'
datatest1: '#0000',
datatest2: '#0000'
}
},
axis: {
x: {
type: 'timeseries',
tick: {
format: '%Y-%m-%d'
}
}
}
});
----- EDIT
I am doing this because the data (including the identifier) is generated dynamically. Thanks
You can dynamically create the colors object like this.
var colors = {};
colors[datatest1[0]] = '#0000';
colors[datatest2[0]] = '#0000';
then set it in the graph like this
var chart = c3.generate({
data: {
x: 'x',
columns: [
axis,
datatest1,
datatest2
],
colors: colors //set colors object created above
},
axis: {
x: {
type: 'timeseries',
tick: {
format: '%Y-%m-%d'
}
}
}
});
working code here

C3.js - stacked bar chart - can I make values to contain instead of stack each other?

I'm using C3.js library to create a stacked bar chart (my current code is in jsfiddle on the bottom). The problem is that by default the columns are... stacked. Since I need to create columns with min, average and max values, I'd like the values to rather contain each other, not stack. E.g. if I have min = 10, average = 50 and max = 100, I'd like the bar to be of the height 100, not 160. Is there any built in way to support such behavior?
My current code:
<div id="chart"></div>
<script>
var chart = c3.generate({
bindTo: '#chart',
data: {
columns: [
['min', 10, 25, 15],
['avg', 50, 33, 51],
['max', 100, 75, 200]
],
type: 'bar',
groups: [
['min', 'avg', 'max']
]
}
});
</script>
Here is a jsfiddle with my code:
https://jsfiddle.net/gguej6n0/
Right, I'm adding this as another answer as it's completely different plus if I changed my original answer the comments would make no sense..
This time I'm taking advantage of err... feature (bug?) found in c3 that caused another user to unintentionally get the effect that you wanted.
c3.js generate a stacked bar from JSON payload
Basically, if you supply the data as json you can get the effect you want if you supply each datum as a separate point
e.g. doing this will overplot min and max on the same column even if they are meant to be stacked
[{ "x-axis": "0",
"min": 30
},
{ "x-axis": "0",
"max": 40
}],
Whereas, this way will stack them:
[{ "x-axis": "0",
"min": 30,
"max": 40
}],
So what we need is a routine to turn the original column-based data into a json object where every datum is parcelled up separately:
http://jsfiddle.net/gvn3y0q6/5/
var data = [
['min', 10, 25, 15, 12],
['avg', 50, 33, 51, 24],
['max', 100, 75, 200, 76]
];
var json = [];
data.forEach (function (datum) {
var series = datum[0];
for (var i = 1; i < datum.length; i++) {
var jdatum = {"x-axis": i-1};
jdatum[series] = datum[i];
json.push (jdatum);
}
});
var chart = c3.generate({
data: {
x: "x-axis",
json:json,
keys: {
x: "x-axis",
value: ["max", "avg", "min"]
},
groups: [
['max', 'avg', 'min']
],
type: 'bar',
},
bar: {
width: {
ratio: 0.95
}
},
axis: {
x: {
padding: {
left: 0.5,
right: 0.5,
}
}
},
tooltip: {
grouped: true,
contents: function (d, defaultTitleFormat, defaultValueFormat, color) {
var data = this.api.data.shown().map (function(series) {
var matchArr = series.values.filter (function (datum) {
return datum.value != undefined && datum.x === d[0].x;
});
matchArr[0].name = series.id;
return matchArr[0];
});
return this.getTooltipContent(data, defaultTitleFormat, defaultValueFormat, color);
}
}
});
This time, hiding a series doesn't affect the other series. There's still some tooltip jiggery-pokery needed though otherwise we only get one of the values reported in the tooltip when hovering over one of the 'stacks'. It looks like the data includes a lot of empty value points, which leads me to think this is a bug of some sort I'm taking advantage of here..
So bearing in mind this might get fixed at some point in the future (or maybe left in, if someone points out it's useful for doing this sort of plot) - then this seems to do what you want
I'm not aware of anything built-in that will make the bars merge into one another as you describe, but could a side-by-side view help? If so, remove the grouping.
See https://jsfiddle.net/gguej6n0/1/
var chart = c3.generate({
bindTo: '#chart',
data: {
columns: [
['min', 10, 25, 15],
['avg', 50, 33, 51],
['max', 100, 75, 200]
],
type: 'bar'
},
bar: {
width: {
ratio: 0.3
}
}
});
I don't think c3 has an 'overplot' bars option, but you could massage your data...
Basically process the data beforehand so max is actually max - avg, and avg is actually avg - min
Then the tooltip routine restores the correct totals for showing to a user
Just be careful if you pass the data onto anything else and remember the data has been changed and restore it (or keep a copy)
https://jsfiddle.net/gguej6n0/5/
var data = [
['min', 10, 25, 15],
['avg', 50, 33, 51],
['max', 100, 75, 200]
];
for (var n = data.length-1; n > 0; n--) {
for (var m = 1; m < data[n].length; m++) {
data[n][m] -= data[n-1][m];
}
}
var chart = c3.generate({
bindTo: '#chart',
data: {
columns: data,
type: 'bar',
groups: [
['min', 'avg', 'max']
]
},
tooltip: {
contents: function (d, defaultTitleFormat, defaultValueFormat, color) {
var dc = d.map (function (dd) {
return {value: dd.value, x: dd.x, id: dd.id, name: dd.name, index: dd.index};
})
for (var n= 1; n < dc.length; n++) {
dc[n].value += dc[n-1].value;
}
return this.getTooltipContent(dc, defaultTitleFormat, defaultValueFormat, color);
}
}
});

C3js Stacked Bar Chart with a hidden bar

Scenario: I need to show a bar chart about payment details, how much the customers paid for a product using Card, Cheque or Cash. And also need to display the total value in a stacked bar chart.
var chart = c3.generate({
data: {
columns: [
['Cash', 30, 200, 100, 400, 150, 250],
['Card', 130, 100, 140, 200, 150, 50],
['Total', 160,300,240,600,300,300]
],
groups:[['Cash','Card']],
type: 'bar'
}
});
Output:
I need to show the Total value but not the bar and the legend. How can do this? Any advice would be helpful. Thank you.
You don't need the total dataset, you can alter the tooltip contents to calculate it on the fly (adapting the technique found in the answer here -> https://stackoverflow.com/a/36789099/368214)
var chart = c3.generate({
data: {
columns: [
['Cash', 30, 200, 100, 400, 150, 250],
['Card', 130, 100, 140, 200, 150, 50],
],
groups:[['Cash','Card']],
type: 'bar'
},
tooltip : {
contents: function (d, defaultTitleFormat, defaultValueFormat, color) {
var total = d.reduce (function(subTotal,b) { return subTotal + b.value; }, 0);
d.push ({value: total, id: "Total", name: "Total", x:d[0].x , index:d[0].index});
return this.getTooltipContent(d, defaultTitleFormat, defaultValueFormat, color);
}
}
});
The total value is calculated using .reduce to sum all the other values at that point. Then make a new data point for the tooltip called 'Total' using that value and pass it to the default renderer for drawing (this.getTooltipContent)
http://jsfiddle.net/vz1rwvwn/

Position an inset legend at the top-center

According to the C3 documentation, legend.inset.postition only supports top-left, top-right, bottom-left and bottom-right positions. I would like to have my legend positioned at the top-center.
Here is the JS that creates the plot:
var chart = c3.generate({
data: {
columns: [
['data1', 100, 200, 50, 300, 200, 50, 250, 100, 200, 400, 50],
['data2', 400, 500, 250, null, 300, 50, 200, 450, 300, 300, 100]
],
},
legend: {
position: 'inset',
inset: {
anchor: 'top-left', // how do I implement 'top-center'?
x: 40,
y: 2,
step: 1
}
}
});
I have attempted to re-position the element after the chart has been rendered via figuring out its current position. However, none of the SVG elements appear to have attributes that enable this kind of introspection:
var legend = d3.select(".c3-legend-background");
console.log("style[width]: " + legend.style("width")); // style[width]: auto
console.log("attr[x]: " + legend.attr("x")); // attr[x]: null
console.log("attr[y]: " + legend.attr("y")); // attr[y]: null
Wouldn't you just set the x value of the inset object to the correct value to get it to center across the top?
This would depend on your wide your chart if and how wide your legend is. So the calculation would be (ChartWidth - LegendWidth) / 2.
The legend object is then something like:
legend: {
position:'inset',
inset: {
anchor: 'top-left',
x: 200 // put the result of your calculation here
}
}
Here's a rough example: http://jsfiddle.net/jrdsxvys/11/

Categories