"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
+ "promise-polyfill": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.1.3.tgz",
+ "integrity": "sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==",
+ "dev": true
+ },
"psl": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
"karma-safari-private-launcher": "^1.0.0",
"moment": "^2.27.0",
"pixelmatch": "^5.2.1",
+ "promise-polyfill": "^8.1.3",
"resize-observer-polyfill": "^1.5.1",
"rollup": "^2.25.0",
"rollup-plugin-babel": "^4.4.0",
input,
plugins: [
inject({
- ResizeObserver: 'resize-observer-polyfill'
+ ResizeObserver: 'resize-observer-polyfill',
+ Promise: 'promise-polyfill'
}),
json(),
resolve(),
input,
plugins: [
inject({
- ResizeObserver: 'resize-observer-polyfill'
+ ResizeObserver: 'resize-observer-polyfill',
+ Promise: 'promise-polyfill'
}),
json(),
resolve(),
initialize() {
const me = this;
+ me.enableOptionSharing = true;
super.initialize();
const horizontal = vscale.isHorizontal();
const ruler = me._getRuler();
const firstOpts = me.resolveDataElementOptions(start, mode);
- const sharedOptions = me.getSharedOptions(mode, rectangles[start], firstOpts);
+ const sharedOptions = me.getSharedOptions(firstOpts);
const includeOptions = me.includeOptions(mode, sharedOptions);
- let i;
+ me.updateSharedOptions(sharedOptions, mode, firstOpts);
- for (i = 0; i < rectangles.length; i++) {
+ for (let i = 0; i < rectangles.length; i++) {
const index = start + i;
- const options = me.resolveDataElementOptions(index, mode);
+ const options = sharedOptions || me.resolveDataElementOptions(index, mode);
const vpixels = me._calculateBarValuePixels(index, options);
const ipixels = me._calculateBarIndexPixels(index, ruler, options);
width: horizontal ? undefined : ipixels.size
};
- // all borders are drawn for floating bar
- /* TODO: float bars border skipping magic
- if (me.getParsed(i)._custom) {
- model.borderSkipped = null;
- }
- */
if (includeOptions) {
properties.options = options;
}
me.updateElement(rectangles[i], index, properties, mode);
}
-
- me.updateSharedOptions(sharedOptions, mode);
}
/**
import {resolveObjectKey} from '../helpers/helpers.core';
export default class BubbleController extends DatasetController {
+ initialize() {
+ this.enableOptionSharing = true;
+ super.initialize();
+ }
/**
* Parse array of objects
const reset = mode === 'reset';
const {xScale, yScale} = me._cachedMeta;
const firstOpts = me.resolveDataElementOptions(start, mode);
- const sharedOptions = me.getSharedOptions(mode, points[start], firstOpts);
+ const sharedOptions = me.getSharedOptions(firstOpts);
const includeOptions = me.includeOptions(mode, sharedOptions);
for (let i = 0; i < points.length; i++) {
me.updateElement(point, index, properties, mode);
}
- me.updateSharedOptions(sharedOptions, mode);
+ me.updateSharedOptions(sharedOptions, mode, firstOpts);
}
/**
constructor(chart, datasetIndex) {
super(chart, datasetIndex);
+ this.enableOptionSharing = true;
this.innerRadius = undefined;
this.outerRadius = undefined;
this.offsetX = undefined;
const innerRadius = animateScale ? 0 : me.innerRadius;
const outerRadius = animateScale ? 0 : me.outerRadius;
const firstOpts = me.resolveDataElementOptions(start, mode);
- const sharedOptions = me.getSharedOptions(mode, arcs[start], firstOpts);
+ const sharedOptions = me.getSharedOptions(firstOpts);
const includeOptions = me.includeOptions(mode, sharedOptions);
let startAngle = opts.rotation;
let i;
innerRadius
};
if (includeOptions) {
- properties.options = me.resolveDataElementOptions(index, mode);
+ properties.options = sharedOptions || me.resolveDataElementOptions(index, mode);
}
startAngle += circumference;
me.updateElement(arc, index, properties, mode);
}
- me.updateSharedOptions(sharedOptions, mode);
+ me.updateSharedOptions(sharedOptions, mode, firstOpts);
}
calculateTotal() {
export default class LineController extends DatasetController {
+ initialize() {
+ this.enableOptionSharing = true;
+ super.initialize();
+ }
+
update(mode) {
const me = this;
const meta = me._cachedMeta;
const reset = mode === 'reset';
const {xScale, yScale, _stacked} = me._cachedMeta;
const firstOpts = me.resolveDataElementOptions(start, mode);
- const sharedOptions = me.getSharedOptions(mode, points[start], firstOpts);
+ const sharedOptions = me.getSharedOptions(firstOpts);
const includeOptions = me.includeOptions(mode, sharedOptions);
const spanGaps = valueOrDefault(me._config.spanGaps, me.chart.options.spanGaps);
const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY;
};
if (includeOptions) {
- properties.options = me.resolveDataElementOptions(index, mode);
+ properties.options = sharedOptions || me.resolveDataElementOptions(index, mode);
}
me.updateElement(point, index, properties, mode);
prevParsed = parsed;
}
- me.updateSharedOptions(sharedOptions, mode);
+ me.updateSharedOptions(sharedOptions, mode, firstOpts);
}
/**
this._prop = prop;
this._from = from;
this._to = to;
+ this._promises = undefined;
}
active() {
// update current evaluated value, for smoother animations
me.tick(Date.now());
me._active = false;
+ me._notify(false);
}
}
if (!me._active) {
me._target[prop] = to;
+ me._notify(true);
return;
}
me._target[prop] = me._fn(from, to, factor);
}
+
+ wait() {
+ const promises = this._promises || (this._promises = []);
+ return new Promise((res, rej) => {
+ promises.push({res, rej});
+ });
+ }
+
+ _notify(resolved) {
+ const method = resolved ? 'res' : 'rej';
+ const promises = this._promises || [];
+ for (let i = 0; i < promises.length; i++) {
+ promises[i][method]();
+ }
+ }
}
function copyOptions(target, values) {
const oldOpts = target.options;
const newOpts = values.options;
- if (!oldOpts || !newOpts || newOpts.$shared) {
+ if (!oldOpts || !newOpts) {
return;
}
- if (oldOpts.$shared) {
+ if (oldOpts.$shared && !newOpts.$shared) {
target.options = Object.assign({}, oldOpts, newOpts, {$shared: false});
} else {
Object.assign(oldOpts, newOpts);
/**
* Utility to handle animation of `options`.
- * This should not be called, when animating $shared options to $shared new options.
* @private
- * @todo if new options are $shared, target.options should be replaced with those new shared
- * options after all animations have completed
*/
_animateOptions(target, values) {
const newOptions = values.options;
- let animations = [];
-
- if (!newOptions) {
- return animations;
+ const options = resolveTargetOptions(target, newOptions);
+ if (!options) {
+ return [];
}
- let options = target.options;
- if (options) {
- if (options.$shared) {
- // If the current / old options are $shared, meaning other elements are
- // using the same options, we need to clone to become unique.
- target.options = options = Object.assign({}, options, {$shared: false, $animations: {}});
- }
- animations = this._createAnimations(options, newOptions);
- } else {
- target.options = newOptions;
+
+ const animations = this._createAnimations(options, newOptions);
+ if (newOptions.$shared && !options.$shared) {
+ // Going from distinct options to shared options:
+ // After all animations are done, assing the shared options object to the element
+ // So any new updates to the shared options are observed
+ awaitAll(target.$animations, newOptions).then(() => {
+ target.options = newOptions;
+ });
}
+
return animations;
}
}
}
+function awaitAll(animations, properties) {
+ const running = [];
+ const keys = Object.keys(properties);
+ for (let i = 0; i < keys.length; i++) {
+ const anim = animations[keys[i]];
+ if (anim && anim.active()) {
+ running.push(anim.wait());
+ }
+ }
+ // @ts-ignore
+ return Promise.all(running);
+}
+
+function resolveTargetOptions(target, newOptions) {
+ if (!newOptions) {
+ return;
+ }
+ let options = target.options;
+ if (!options) {
+ target.options = newOptions;
+ return;
+ }
+ if (options.$shared && !newOptions.$shared) {
+ // Going from shared options to distinct one:
+ // Create new options object containing the old shared values and start updating that.
+ target.options = options = Object.assign({}, options, {$shared: false, $animations: {}});
+ }
+ return options;
+}
import {requestAnimFrame} from '../helpers/helpers.extras';
/**
+ * @typedef { import("./core.animation").default } Animation
* @typedef { import("./core.controller").default } Chart
*/
me._updateLayout();
// Can only reset the new controllers after the scales have been updated
- if (me.options.animation) {
- each(newControllers, (controller) => {
- controller.reset();
- });
- }
+ each(newControllers, (controller) => {
+ controller.reset();
+ });
me._updateDatasets(mode);
return active ? 'hover' + _capitalize(key) : key;
}
+function isDirectUpdateMode(mode) {
+ return mode === 'reset' || mode === 'none';
+}
+
export default class DatasetController {
/**
this._parsing = false;
this._data = undefined;
this._objectData = undefined;
+ this._sharedOptions = undefined;
+ this.enableOptionSharing = false;
this.initialize();
}
me.configure();
me._cachedAnimations = {};
me._cachedDataOpts = {};
- me.update(mode);
+ me.update(mode || 'default');
meta._clip = toClip(valueOrDefault(me._config.clip, defaultClip(meta.xScale, meta.yScale, me.getMaxOverflow())));
}
if (active) {
me._addAutomaticHoverColors(index, options);
}
+
return options;
}
* @protected
*/
resolveDataElementOptions(index, mode) {
+ mode = mode || 'default';
const me = this;
const active = mode === 'active';
const cached = me._cachedDataOpts;
+ const sharing = me.enableOptionSharing;
if (cached[mode]) {
return cached[mode];
}
if (info.cacheable) {
// `$shared` indicades this set of options can be shared between multiple elements.
// Sharing is used to reduce number of properties to change during animation.
- values.$shared = true;
+ values.$shared = sharing;
// We cache options by `mode`, which can be 'active' for example. This enables us
// to have the 'active' element options and 'default' options to switch between
// when interacting.
- cached[mode] = values;
+ cached[mode] = sharing ? Object.freeze(values) : values;
}
return values;
}
/**
- * Utility for checking if the options are shared and should be animated separately.
+ * Utility for getting the options object shared between elements
* @protected
*/
- getSharedOptions(mode, el, options) {
- if (!mode) {
- // store element option sharing status for usage in interactions
- this._sharedOptions = options && options.$shared;
- }
- if (mode !== 'reset' && options && options.$shared && el && el.options && el.options.$shared) {
- return {target: el.options, options};
+ getSharedOptions(options) {
+ if (!options.$shared) {
+ return;
}
+ return this._sharedOptions || (this._sharedOptions = Object.assign({}, options));
}
/**
* @protected
*/
includeOptions(mode, sharedOptions) {
- if (mode === 'hide' || mode === 'show') {
- return true;
- }
- return mode !== 'resize' && !sharedOptions;
+ return !sharedOptions || isDirectUpdateMode(mode);
}
/**
* @protected
*/
updateElement(element, index, properties, mode) {
- if (mode === 'reset' || mode === 'none') {
+ if (isDirectUpdateMode(mode)) {
Object.assign(element, properties);
} else {
this._resolveAnimations(index, mode).update(element, properties);
* Utility to animate the shared options, that are potentially affecting multiple elements.
* @protected
*/
- updateSharedOptions(sharedOptions, mode) {
+ updateSharedOptions(sharedOptions, mode, newOptions) {
if (sharedOptions) {
- this._resolveAnimations(undefined, mode).update(sharedOptions.target, sharedOptions.options);
+ this._resolveAnimations(undefined, mode).update({options: sharedOptions}, {options: newOptions});
}
}
*/
_setStyle(element, index, mode, active) {
element.active = active;
- this._resolveAnimations(index, mode, active).update(element, {options: this.getStyle(index, active)});
+ const options = this.getStyle(index, active);
+ this._resolveAnimations(index, mode, active).update(element, {options: this.getSharedOptions(options) || options});
}
removeHoverStyle(element, datasetIndex, index) {
options: undefined
})).toBeUndefined();
});
+
+ it('should assign shared options to target after animations complete', function(done) {
+ const chart = {
+ draw: function() {},
+ options: {
+ animation: {
+ debug: false
+ }
+ }
+ };
+ const anims = new Chart.Animations(chart, {value: {duration: 100}, option: {duration: 200}});
+
+ const target = {
+ value: 1,
+ options: {
+ option: 2
+ }
+ };
+ const sharedOpts = {option: 10, $shared: true};
+
+ expect(anims.update(target, {
+ options: sharedOpts
+ })).toBeTrue();
+
+ expect(target.options !== sharedOpts).toBeTrue();
+
+ Chart.animator.start(chart);
+
+ setTimeout(function() {
+ expect(Chart.animator.running(chart)).toBeFalse();
+ expect(target.options === sharedOpts).toBeTrue();
+
+ Chart.animator.remove(chart);
+ done();
+ }, 300);
+ });
});
readonly chart: Chart;
readonly index: number;
readonly _cachedMeta: IChartMeta<E, DSE>;
+ enableOptionSharing: boolean;
linkScales(): void;
getAllParsedValues(scale: Scale): number[];
* Utility for checking if the options are shared and should be animated separately.
* @protected
*/
- protected getSharedOptions(mode: UpdateMode, el: E, options: any): undefined | { target: any; options: any };
+ protected getSharedOptions(options: any): undefined | { any };
/**
* Utility for determining if `options` should be included in the updated properties
* @protected
* @protected
*/
- protected updateSharedOptions(sharedOptions: any, mode: UpdateMode): void;
+ protected updateSharedOptions(sharedOptions: any, mode: UpdateMode, newOptions: any): void;
removeHoverStyle(element: E, datasetIndex: number, index: number): void;
setHoverStyle(element: E, datasetIndex: number, index: number): void;