Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix scale options update error #4198

Merged
merged 3 commits into from Nov 29, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 62 additions & 3 deletions docs/developers/updates.md
@@ -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

Expand All @@ -14,9 +14,7 @@ function addData(chart, label, data) {
});
chart.update();
}
```

```javascript
function removeData(chart) {
chart.data.labels.pop();
chart.data.datasets.forEach((dataset) => {
Expand All @@ -26,6 +24,67 @@ 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.
- 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'];
}
```

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.
3 changes: 3 additions & 0 deletions samples/samples.js
Expand Up @@ -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',
Expand Down
104 changes: 104 additions & 0 deletions samples/scales/toggle-scale-type.html
@@ -0,0 +1,104 @@
<!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 = {
xAxes: [{
display: true
}],
yAxes: [{
display: true,
type: type

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to make just the assignment + update work ? (detect one scale was dirtied and rebuild that one)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, just

        window.myLine.options.scales.yAxes[0] = {
            display: true,
            type: type
        };

Please create a new object for scale, Chat.js will calculate all the missing config for you. If you just modify the 'type' attribute instead of a new object, Chart.js assumes you don't want all the previous calculated value to be changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or if you have multiple axes, assigning an id would be easier than index.

Copy link

@ldemailly ldemailly Nov 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh nice, that should simplify my code, will try! (it's not super obvious) - why is display: true needed ?

to make the example easier you could consider adding other attributes to the "before toggle" options that will help readers notice they stay

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I was just meant all the options other than the default should remain. Will change the example and doc later.

}]
}

window.myLine.update();
});
</script>
</body>

</html>
74 changes: 51 additions & 23 deletions src/core/core.controller.js
Expand Up @@ -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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since a new object has already been created, it should be more clear to just reset the options, the result is actually the same.

chart.ensureScalesHaveIDs();
chart.buildOrUpdateScales();
// Tooltip
chart.tooltip._options = newOptions.tooltips;
chart.tooltip.initialize();
}

function positionIsHorizontal(position) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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)
Expand All @@ -278,6 +297,14 @@ module.exports = function(Chart) {
me.scale = scale;
}
});
// clear up discarded scales
helpers.each(updated, function(hasUpdated, id) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@etimberg last commit here was definitely a mistake. Fixed here.
So now whenever user specify a scale with a new id or the type is changed, user lose the reference to the old scale.

if (!hasUpdated) {
delete scales[id];
}
});

me.scales = scales;

Chart.scaleService.addScalesToLayout(this);
},
Expand All @@ -301,6 +328,7 @@ module.exports = function(Chart) {

if (meta.controller) {
meta.controller.updateIndex(datasetIndex);
meta.controller.linkScales();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came across this when I tried to update scales with different id - dataset._meta contains scales id which didn't update.
I check the scales id of meta here, relink the scale if discarded.

I'm not sure whether I missed anything else, please point out if there are. It's a little bit too many configs here : P

} else {
var ControllerClass = Chart.controllers[meta.type];
if (ControllerClass === undefined) {
Expand Down
4 changes: 2 additions & 2 deletions src/core/core.datasetController.js
Expand Up @@ -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;
}
},
Expand Down