I'm seeing an awkward bug using a third party library inside of a react component. I was able to reproduce a contrived demo for this post.
Let me start by explaining that I am using c3js - which is a charting library and rendering it in componentDidMount() and removing it in componentWillUnmount() with the correct calls to this.chart.destroy().
The bug occurs when filtering the components themselves, essentially what happens is the components are filtered correctly but the actual chart that sits inside the component stays the same as the first chart, which is very strange behaviour. Basically it's the wrong chart inside the wrong component!
You can see what I mean by clicking on the Remove all charts except chart 3 button, I have labeled the charts with a chartid and the filtering will correctly remove the other charts.
I am fairly certain it isn't my code because the filtering works correctly and updates the view. You can verify because I have labeled the charts and it is visible in the view. There is no console errors and I have verified my code works.
So my question is - can we work around this limitation using c3js, or is this really a problem with my code and the way I am rendering the charts.
Related demo: https://jsfiddle.net/69z2wepo/38614/
Related code:
var data = [
{
chartid: 1,
columns: [
['x', 0, 1, 2, 3, 4, 5, 6],
['data1', 130, 300, 330, 400, 300, 400, 500],
['data2', 390, 230, 200, 150, 100, 130, 210],
['data3', 290, 430, 300, 160, 210, 170, 190],
['data4', 190, 330, 200, 260, 190, 250, 320]
]
},
{
chartid: 2,
columns: [
['x', 0, 1, 2, 3, 4, 5, 6],
['data1', 130, 300, 330, 400, 300, 400, 500],
['data2', 390, 230, 200, 150, 100, 130, 210],
['data3', 290, 430, 300, 160, 210, 170, 190]
]
},
{
chartid: 3,
columns: [
['x', 0, 1, 2, 3, 4, 5, 6],
['data1', 130, 300, 330, 400, 300, 400, 500],
['data2', 390, 230, 200, 150, 100, 130, 210]
]
}
];
var Button = React.createClass({
handleDelete: function (id) {
this.props.handleDelete(id);
},
render: function() {
return (
<button onClick={this.handleDelete.bind(null, 3)}>
Remove all charts except chart 3
</button>
)
}
});
var Chart = React.createClass({
componentDidMount() {
this.chart = c3.generate({
bindto: '.chart-' + this.props.data.chartid,
data: {
columns: this.props.data.columns
}
});
},
componentWillUnmount() {
this.chart.destroy();
},
render: function() {
return (
<div>
<h4>{"chart-" + this.props.data.chartid}</h4>
<div className={"chart-" + this.props.data.chartid}>
</div>
</div>
)
}
});
var Child = React.createClass({
renderCharts: function(data) {
return data.map(function(metrics, i) {
return (
<Chart key={i} data={metrics} />
)
});
},
handleDelete: function(id) {
this.props.handleDelete(id);
},
render: function() {
return (
<div>
<Button handleDelete={this.handleDelete} />
{this.renderCharts(this.props.data)}
</div>
)
}
})
var App = React.createClass({
getInitialState: function() {
return {
initialData: this.props.data
}
},
handleDelete: function(id) {
var _filterFunc = function(data) {
if (data.chartid == id) return true;
return false;
};
var _filterCharts = Array.prototype.filter.call(this.state.initialData, _filterFunc);
this.setState({
initialData: _filterCharts
})
},
render: function() {
return (
<div>
<Child handleDelete={this.handleDelete} data={this.state.initialData} />
</div>
);
}
});
ReactDOM.render(
<App data={data} />,
document.getElementById('container')
);
The problem is the way you are setting the key on your chart. It's causing the renderer to get confused about which chart you're trying to keep.
Try this:
<Chart key={data[i].chartid} data={metrics} />
instead of <Chart key={i} data={metrics} />
Take a look at how React handles keys. Remember that you're uniquely identifying a child with a key for the lifecycle of the component. So since chart 1 is uniquely identified by key "1", you can't render chart 3 with key "1." My solution above ensures that the chart is uniquely identified by its chart id instead of by its rendering order.
Related
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.
I want to create a char using c3 in React to, later on, be updated each second.
I am trying to follow the example provided by
Updating C3 charts on props change with React, but the first step, which is to create the chart, is not happening.
This is my fiddle:
https://jsfiddle.net/69z2wepo/227446/
import c3 from 'c3';
import React from "react";
import ReactDOM from "react-dom";
class Hello extends React.Component {
renderChart() {
this.chart = c3.generate({
bindto:"#chart1",
data: {
columns: [
['data1', 30, 200, 100, 400, 150, 250],
['data2', 50, 20, 10, 40, 15, 25]
]
}
});
}
render() {
this.renderChart()
return <div id="chart1"></div>;
}
}
ReactDOM.render(
<Hello />,
document.getElementById('container')
);
I installed c3 with npm and am importing it in the component.
Thanks for the help.
In your example, it looks like the chart is generated before the selector div is even rendered, so the chart has no where to go. Instead of calling this.renderChart() in render(), you can call it componentDidMount. In that case, render will be called on the initial load, your <div id="chart1"></div> will be rendered and then renderChart will run, adding the SVG to the div.
As for updating the data, you can move the column data itself to state, then call setState with some new data and use componentDidUpdate to rerender the chart. That might look something like this:
class Chart extends React.Component {
constructor(props) {
super(props);
this.state = {
column1: ['data1', 30, 200, 100, 400, 150, 250],
column2: ['data2', 50, 20, 10, 40, 15, 25],
};
this.changeData = this.changeData.bind(this);
}
renderChart() {
c3.generate({
bindto: "#chart1",
data: {
columns: [this.state.column1, this.state.column2],
},
});
}
componentDidMount() {
this.renderChart();
}
componentDidUpdate() {
this.renderChart();
}
// Changes data to something arbitrary on button click
changeData() {
this.setState({
column1: ['data1', 70, 120, 30, 300, 230, 300],
column2: ['data2', 100, 120, 50, 140, 150, 80],
});
}
render() {
return (
<div>
<div id="chart1"></div>
<button onClick={this.changeData}>Change</button>
</div>
);
}
}
React Lifecycle methods are key here. Here's a handy chart linked from the docs: http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
I have the following json array of which each object is accessed on a specific tab click. Each json object of the array has different templates like they can represent ui-grid, or c3 chart directive (c3.js), etc
Now when the user clicks specific tab, this template which is currently in in string format needs to be made to render actual templates like ui-grid, c3-chart, etc
var sampleJsonArray = [{
id: 0,
tabName: "Table",
template: ' <div id="queryListGrid" ui-grid="queryListGridOptions" class="grid query-list-grid"></div>'
},
{
id: 1,
tabName: "Cost - Line",
template: ' <c3-simple id="view1" config="c3ChartCost"></c3-simple> '
},
{
id: 2,
tabName: "Spend - Bar",
template: ' <c3-simple id="view2" config="c3ChartSpend"></c3-simple> '
},
];
The following are each tabs templates sample jsons:
$scope.queryListGridOptions = {
enableSorting: true,
columnDefs: [{
field: 'name'
},
{
field: 'gender'
},
{
field: 'company',
enableSorting: false
}
],
data: //data will be fetched from http call -- []
onRegisterApi: function(gridApi) {
$scope.grid1Api = gridApi;
}
};
$scope.c3ChartCost = {
size: {
height: 250,
width: 200
},
data: {
columns: [
['data1', 30, 200, 100, 400, 150, 250],
['data2', 130, 100, 140, 200, 150, 50]
],
type: 'line'
}
};
$scope.c3ChartSpend = {
size: {
height: 250,
width: 200
},
data: {
columns: [
['data1', 5, 510, 160, 700, 190, 960],
['data2', 87, 450, 56, 780, 670, 890]
],
type: 'bar'
}
};
You might have to use $sce service provided by angular. Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain contexts to result in a value that is marked as safe to use for that context.
I dont have much time to provide you with a working code snippet right now. Checkout this documentation from angular. Let me know if you need any help later.
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.
If I have a C3JS grouped bar chart defined like the following, how can I get the segments to stay in the order I've defined them instead of in ascending order by value? By default C3 will order them as 5, 10, 40, but I want it to remain as 10, 40, 5.
c3.generate({
bindto: '.active-loads',
data: {
columns: [
['Picking up future', 10],
['Enroute', 40],
['Delivered', 5]
],
type: 'bar',
groups: [
['Picking up future', 'Enroute', 'Delivered']
],
onclick: function(d) {
console.debug(d);
}
},
axis: {
rotated: true,
x: {
show: false
}
}
});
EDIT
Turns out it's as easy as specifying order: null in the data property.
C3js documentation has page for this : http://c3js.org/samples/data_order.html
You can order your data in following way :
var chart = c3.generate({
data: {
columns: [
['data1', 130, 200, 320, 400, 530, 750],
['data2', -130, 10, 130, 200, 150, 250],
['data3', -130, -50, -10, -200, -250, -150]
],
type: 'bar',
groups: [
['data1', 'data2', 'data3']
],
order: 'desc' // stack order by sum of values descendantly.
// order: 'asc' // stack order by sum of values ascendantly.
// order: null // stack order by data definition.
},
grid: {
y: {
lines: [{value:0}]
}
}
});
Also detailed explanation here too : http://c3js.org/reference.html#data-order
You can specify your function too :)