Fix scale options update (#4198)

- allow options to be updated in-place or as a new object
- re-merge new options and rebuild scales & tooltips
- preserve reference to old scale if id/type not changed
- related tests and new sample also added.
- update document about options update
- update doc and example
This commit is contained in:
Xingan Wang 2017-11-29 13:52:23 -08:00 committed by Simon Brunel
parent 42d3d91f52
commit 333f2eba99
6 changed files with 332 additions and 27 deletions

View File

@ -1,6 +1,6 @@
# Updating Charts
It's pretty common to want to update charts after they've been created. When the chart data is changed, Chart.js will animate to the new data values.
It's pretty common to want to update charts after they've been created. When the chart data or options are changed, Chart.js will animate to the new data values and options.
## Adding or Removing Data
@ -14,9 +14,7 @@ function addData(chart, label, data) {
});
chart.update();
}
```
```javascript
function removeData(chart) {
chart.data.labels.pop();
chart.data.datasets.forEach((dataset) => {
@ -26,6 +24,78 @@ function removeData(chart) {
}
```
## Updating Options
To update the options, mutating the options property in place or passing in a new options object are supported.
- If the options are mutated in place, other option properties would be preserved, including those calculated by Chart.js.
- If created as a new object, it would be like creating a new chart with the options - old options would be discarded.
```javascript
function updateConfigByMutating(chart) {
chart.options.title.text = 'new title';
chart.update();
}
function updateConfigAsNewObject(chart) {
chart.options = {
responsive: true,
title:{
display:true,
text: 'Chart.js'
},
scales: {
xAxes: [{
display: true
}],
yAxes: [{
display: true
}]
}
}
chart.update();
}
```
Scales can be updated separately without changing other options.
To update the scales, pass in an object containing all the customization including those unchanged ones.
Variables referencing any one from `chart.scales` would be lost after updating scales with a new `id` or the changed `type`.
```javascript
function updateScales(chart) {
var xScale = chart.scales['x-axis-0'];
var yScale = chart.scales['y-axis-0'];
chart.options.scales = {
xAxes: [{
id: 'newId',
display: true
}],
yAxes: [{
display: true,
type: 'logarithmic'
}]
}
chart.update();
// need to update the reference
xScale = chart.scales['newId'];
yScale = chart.scales['y-axis-0'];
}
```
You can also update a specific scale either by specifying its index or id.
```javascript
function updateScale(chart) {
chart.options.scales.yAxes[0] = {
type: 'logarithmic'
}
chart.update();
}
```
Code sample for updating options can be found in [toggle-scale-type.html](../../samples/scales/toggle-scale-type.html).
## Preventing Animations
Sometimes when a chart updates, you may not want an animation. To achieve this you can call `update` with a duration of `0`. This will render the chart synchronously and without an animation.

View File

@ -136,6 +136,9 @@
}, {
title: 'Non numeric Y Axis',
path: 'scales/non-numeric-y.html'
}, {
title: 'Toggle Scale Type',
path: 'scales/toggle-scale-type.html'
}]
}, {
title: 'Legend',

View File

@ -0,0 +1,99 @@
<!doctype html>
<html>
<head>
<title>Toggle Scale Type</title>
<script src="../../dist/Chart.bundle.js"></script>
<script src="../utils.js"></script>
<style>
canvas {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>
</head>
<body>
<div style="width:75%;">
<canvas id="canvas"></canvas>
</div>
<button id="toggleScale">Toggle Scale Type</button>
<script>
var randomScalingFactor = function() {
return Math.ceil(Math.random() * 10.0) * Math.pow(10, Math.ceil(Math.random() * 5));
};
var type = 'linear';
var config = {
type: 'line',
data: {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [{
label: "My First dataset",
backgroundColor: window.chartColors.red,
borderColor: window.chartColors.red,
fill: false,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
],
}, {
label: "My Second dataset",
backgroundColor: window.chartColors.blue,
borderColor: window.chartColors.blue,
fill: false,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
],
}]
},
options: {
responsive: true,
title:{
display: true,
text: 'Chart.js Line Chart - ' + type
},
scales: {
xAxes: [{
display: true,
}],
yAxes: [{
display: true,
type: type
}]
}
}
};
window.onload = function() {
var ctx = document.getElementById("canvas").getContext("2d");
window.myLine = new Chart(ctx, config);
};
document.getElementById('toggleScale').addEventListener('click', function() {
type = type === 'linear' ? 'logarithmic' : 'linear';
window.myLine.options.title.text = 'Chart.js Line Chart - ' + type;
window.myLine.options.scales.yAxes[0] = {
display: true,
type: type
}
window.myLine.update();
});
</script>
</body>
</html>

View File

@ -45,17 +45,21 @@ module.exports = function(Chart) {
function updateConfig(chart) {
var newOptions = chart.options;
// Update Scale(s) with options
if (newOptions.scale) {
chart.scale.options = newOptions.scale;
} else if (newOptions.scales) {
newOptions.scales.xAxes.concat(newOptions.scales.yAxes).forEach(function(scaleOptions) {
chart.scales[scaleOptions.id].options = scaleOptions;
});
}
helpers.each(chart.scales, function(scale) {
Chart.layoutService.removeBox(chart, scale);
});
newOptions = helpers.configMerge(
Chart.defaults.global,
Chart.defaults[chart.config.type],
newOptions);
chart.options = chart.config.options = newOptions;
chart.ensureScalesHaveIDs();
chart.buildOrUpdateScales();
// Tooltip
chart.tooltip._options = newOptions.tooltips;
chart.tooltip.initialize();
}
function positionIsHorizontal(position) {
@ -143,7 +147,7 @@ module.exports = function(Chart) {
// Make sure scales have IDs and are built before we build any controllers.
me.ensureScalesHaveIDs();
me.buildScales();
me.buildOrUpdateScales();
me.initToolTip();
// After init plugin notification
@ -223,11 +227,15 @@ module.exports = function(Chart) {
/**
* Builds a map of scale ID to scale object for future lookup.
*/
buildScales: function() {
buildOrUpdateScales: function() {
var me = this;
var options = me.options;
var scales = me.scales = {};
var scales = me.scales || {};
var items = [];
var updated = Object.keys(scales).reduce(function(obj, id) {
obj[id] = false;
return obj;
}, {});
if (options.scales) {
items = items.concat(
@ -251,24 +259,35 @@ module.exports = function(Chart) {
helpers.each(items, function(item) {
var scaleOptions = item.options;
var id = scaleOptions.id;
var scaleType = helpers.valueOrDefault(scaleOptions.type, item.dtype);
var scaleClass = Chart.scaleService.getScaleConstructor(scaleType);
if (!scaleClass) {
return;
}
if (positionIsHorizontal(scaleOptions.position) !== positionIsHorizontal(item.dposition)) {
scaleOptions.position = item.dposition;
}
var scale = new scaleClass({
id: scaleOptions.id,
options: scaleOptions,
ctx: me.ctx,
chart: me
});
updated[id] = true;
var scale = null;
if (id in scales && scales[id].type === scaleType) {
scale = scales[id];
scale.options = scaleOptions;
scale.ctx = me.ctx;
scale.chart = me;
} else {
var scaleClass = Chart.scaleService.getScaleConstructor(scaleType);
if (!scaleClass) {
return;
}
scale = new scaleClass({
id: id,
type: scaleType,
options: scaleOptions,
ctx: me.ctx,
chart: me
});
scales[scale.id] = scale;
}
scales[scale.id] = scale;
scale.mergeTicksOptions();
// TODO(SB): I think we should be able to remove this custom case (options.scale)
@ -278,6 +297,14 @@ module.exports = function(Chart) {
me.scale = scale;
}
});
// clear up discarded scales
helpers.each(updated, function(hasUpdated, id) {
if (!hasUpdated) {
delete scales[id];
}
});
me.scales = scales;
Chart.scaleService.addScalesToLayout(this);
},
@ -301,6 +328,7 @@ module.exports = function(Chart) {
if (meta.controller) {
meta.controller.updateIndex(datasetIndex);
meta.controller.linkScales();
} else {
var ControllerClass = Chart.controllers[meta.type];
if (ControllerClass === undefined) {

View File

@ -111,10 +111,10 @@ module.exports = function(Chart) {
var meta = me.getMeta();
var dataset = me.getDataset();
if (meta.xAxisID === null) {
if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) {
meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id;
}
if (meta.yAxisID === null) {
if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) {
meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id;
}
},

View File

@ -775,6 +775,38 @@ describe('Chart', function() {
});
describe('config update', function() {
it ('should update options', function() {
var chart = acquireChart({
type: 'line',
data: {
labels: ['A', 'B', 'C', 'D'],
datasets: [{
data: [10, 20, 30, 100]
}]
},
options: {
responsive: true
}
});
chart.options = {
responsive: false,
scales: {
yAxes: [{
ticks: {
min: 0,
max: 10
}
}]
}
};
chart.update();
var yScale = chart.scales['y-axis-0'];
expect(yScale.options.ticks.min).toBe(0);
expect(yScale.options.ticks.max).toBe(10);
});
it ('should update scales options', function() {
var chart = acquireChart({
type: 'line',
@ -798,6 +830,79 @@ describe('Chart', function() {
expect(yScale.options.ticks.max).toBe(10);
});
it ('should update scales options from new object', function() {
var chart = acquireChart({
type: 'line',
data: {
labels: ['A', 'B', 'C', 'D'],
datasets: [{
data: [10, 20, 30, 100]
}]
},
options: {
responsive: true
}
});
var newScalesConfig = {
yAxes: [{
ticks: {
min: 0,
max: 10
}
}]
};
chart.options.scales = newScalesConfig;
chart.update();
var yScale = chart.scales['y-axis-0'];
expect(yScale.options.ticks.min).toBe(0);
expect(yScale.options.ticks.max).toBe(10);
});
it ('should remove discarded scale', function() {
var chart = acquireChart({
type: 'line',
data: {
labels: ['A', 'B', 'C', 'D'],
datasets: [{
data: [10, 20, 30, 100]
}]
},
options: {
responsive: true,
scales: {
yAxes: [{
id: 'yAxis0',
ticks: {
min: 0,
max: 10
}
}]
}
}
});
var newScalesConfig = {
yAxes: [{
ticks: {
min: 0,
max: 10
}
}]
};
chart.options.scales = newScalesConfig;
chart.update();
var yScale = chart.scales.yAxis0;
expect(yScale).toBeUndefined();
var newyScale = chart.scales['y-axis-0'];
expect(newyScale.options.ticks.min).toBe(0);
expect(newyScale.options.ticks.max).toBe(10);
});
it ('should update tooltip options', function() {
var chart = acquireChart({
type: 'line',