Skip to content

Commit

Permalink
Early data parsing, stacking by value and support object data (#6576)
Browse files Browse the repository at this point in the history
* Early data parsing + stacking by value
* Review comments
* review comments
* Remove reduntant parsing
* Couple CC warnings
* Optimize filterBetween
* More migration info
  • Loading branch information
kurkle authored and etimberg committed Nov 4, 2019
1 parent dd8d267 commit 72df272
Show file tree
Hide file tree
Showing 28 changed files with 1,038 additions and 727 deletions.
4 changes: 2 additions & 2 deletions docs/developers/axes.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ To work with Chart.js, custom scale types must implement the following interface
// buildTicks() should create a ticks array on the axis instance, if you intend to use any of the implementations from the base class
buildTicks: function() {},

// Get the value to show for the data at the given index of the the given dataset, ie this.chart.data.datasets[datasetIndex].data[index]
getLabelForIndex: function(index, datasetIndex) {},
// Get the label to show for the given value
getLabelForValue: function(value) {},

// Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value
// @param index: index into the ticks array
Expand Down
11 changes: 9 additions & 2 deletions docs/getting-started/v3-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ Chart.js is no longer providing the `Chart.bundle.js` and `Chart.bundle.min.js`.
* `helpers.numberOfLabelLines`
* `helpers.removeEvent`
* `helpers.scaleMerge`
* `scale.getRightValue`
* `scale.mergeTicksOptions`
* `scale.ticksAsNumbers`
* `Chart.Controller`
* `Chart.chart.chart`
* `Chart.types`
* `Line.calculatePointY`
* Made `scale.handleDirectionalChanges` private
* Made `scale.tickValues` private

Expand All @@ -74,13 +76,18 @@ Chart.js is no longer providing the `Chart.bundle.js` and `Chart.bundle.min.js`.

### Changed

#### Ticks
#### Scales

* `scale.getLabelForIndex` was replaced by `scale.getLabelForValue`
* `scale.getPixelForValue` now has only one parameter

##### Ticks

* `scale.ticks` now contains objects instead of strings
* `buildTicks` is now expected to return tick objects
* `afterBuildTicks` now has no parameters like the other callbacks
* `convertTicksToLabels` was renamed to `generateTickLabels`. It is now expected to set the label property on the ticks given as input

#### Time Scale
##### Time Scale

* `getValueForPixel` now returns milliseconds since the epoch
147 changes: 101 additions & 46 deletions src/controllers/controller.bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,57 @@ function computeFlexCategoryTraits(index, ruler, options) {
};
}

function parseFloatBar(arr, item, vScale, i) {
var startValue = vScale._parse(arr[0], i);
var endValue = vScale._parse(arr[1], i);
var min = Math.min(startValue, endValue);
var max = Math.max(startValue, endValue);
var barStart = min;
var barEnd = max;

if (Math.abs(min) > Math.abs(max)) {
barStart = max;
barEnd = min;
}

// Store `barEnd` (furthest away from origin) as parsed value,
// to make stacking straight forward
item[vScale.id] = barEnd;

item._custom = {
barStart: barStart,
barEnd: barEnd,
start: startValue,
end: endValue,
min: min,
max: max
};
}

function parseArrayOrPrimitive(meta, data, start, count) {
var iScale = this._getIndexScale();
var vScale = this._getValueScale();
var labels = iScale._getLabels();
var singleScale = iScale === vScale;
var parsed = [];
var i, ilen, item, entry;

for (i = start, ilen = start + count; i < ilen; ++i) {
entry = data[i];
item = {};
item[iScale.id] = singleScale || iScale._parse(labels[i], i);

if (helpers.isArray(entry)) {
parseFloatBar(entry, item, vScale, i);
} else {
item[vScale.id] = vScale._parse(entry, i);
}

parsed.push(item);
}
return parsed;
}

module.exports = DatasetController.extend({

dataElementType: elements.Rectangle,
Expand All @@ -144,6 +195,24 @@ module.exports = DatasetController.extend({
'minBarLength'
],

/**
* Overriding primitive data parsing since we support mixed primitive/array
* data for float bars
* @private
*/
_parsePrimitiveData: function() {
return parseArrayOrPrimitive.apply(this, arguments);
},

/**
* Overriding array data parsing since we support mixed primitive/array
* data for float bars
* @private
*/
_parseArrayData: function() {
return parseArrayOrPrimitive.apply(this, arguments);
},

initialize: function() {
var me = this;
var meta;
Expand Down Expand Up @@ -183,7 +252,8 @@ module.exports = DatasetController.extend({
label: me.chart.data.labels[index]
};

if (helpers.isArray(dataset.data[index])) {
// all borders are drawn for floating bar
if (me._getParsed(index)._custom) {
rectangle._model.borderSkipped = null;
}

Expand All @@ -202,8 +272,8 @@ module.exports = DatasetController.extend({
var base = vscale.getBasePixel();
var horizontal = vscale.isHorizontal();
var ruler = me._ruler || me.getRuler();
var vpixels = me.calculateBarValuePixels(me.index, index, options);
var ipixels = me.calculateBarIndexPixels(me.index, index, ruler, options);
var vpixels = me.calculateBarValuePixels(index, options);
var ipixels = me.calculateBarIndexPixels(index, ruler, options);

model.horizontal = horizontal;
model.base = reset ? base : vpixels.base;
Expand Down Expand Up @@ -283,7 +353,7 @@ module.exports = DatasetController.extend({
var i, ilen;

for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) {
pixels.push(scale.getPixelForValue(null, i, me.index));
pixels.push(scale.getPixelForValue(me._getParsed(i)[scale.id]));
}

return {
Expand All @@ -299,52 +369,39 @@ module.exports = DatasetController.extend({
* Note: pixel values are not clamped to the scale area.
* @private
*/
calculateBarValuePixels: function(datasetIndex, index, options) {
calculateBarValuePixels: function(index, options) {
var me = this;
var chart = me.chart;
var scale = me._getValueScale();
var isHorizontal = scale.isHorizontal();
var datasets = chart.data.datasets;
var metasets = scale._getMatchingVisibleMetas(me._type);
var value = scale._parseValue(datasets[datasetIndex].data[index]);
var valueScale = me._getValueScale();
var minBarLength = options.minBarLength;
var stacked = scale.options.stacked;
var stack = me.getMeta().stack;
var start = value.start === undefined ? 0 : value.max >= 0 && value.min >= 0 ? value.min : value.max;
var length = value.start === undefined ? value.end : value.max >= 0 && value.min >= 0 ? value.max - value.min : value.min - value.max;
var ilen = metasets.length;
var i, imeta, ivalue, base, head, size, stackLength;

if (stacked || (stacked === undefined && stack !== undefined)) {
for (i = 0; i < ilen; ++i) {
imeta = metasets[i];

if (imeta.index === datasetIndex) {
break;
}

if (imeta.stack === stack) {
stackLength = scale._parseValue(datasets[imeta.index].data[index]);
ivalue = stackLength.start === undefined ? stackLength.end : stackLength.min >= 0 && stackLength.max >= 0 ? stackLength.max : stackLength.min;
var start = 0;
var parsed = me._getParsed(index);
var value = parsed[valueScale.id];
var custom = parsed._custom;
var length = me._cachedMeta._stacked ? me._applyStack(valueScale, parsed) : parsed[valueScale.id];
var base, head, size;

if (length !== value) {
start = length - value;
length = value;
}

if ((value.min < 0 && ivalue < 0) || (value.max >= 0 && ivalue > 0)) {
start += ivalue;
}
}
if (custom && custom.barStart !== undefined && custom.barEnd !== undefined) {
value = custom.barStart;
length = custom.barEnd - custom.barStart;
// bars crossing origin are not stacked
if (value !== 0 && Math.sign(value) !== Math.sign(custom.barEnd)) {
start = 0;
}
start += value;
}

base = scale.getPixelForValue(start);
head = scale.getPixelForValue(start + length);
base = valueScale.getPixelForValue(start);
head = valueScale.getPixelForValue(start + length);
size = head - base;

if (minBarLength !== undefined && Math.abs(size) < minBarLength) {
size = minBarLength;
if (length >= 0 && !isHorizontal || length < 0 && isHorizontal) {
head = base - minBarLength;
} else {
head = base + minBarLength;
}
size = size < 0 ? -minBarLength : minBarLength;
head = base + size;
}

return {
Expand All @@ -358,13 +415,13 @@ module.exports = DatasetController.extend({
/**
* @private
*/
calculateBarIndexPixels: function(datasetIndex, index, ruler, options) {
calculateBarIndexPixels: function(index, ruler, options) {
var me = this;
var range = options.barThickness === 'flex'
? computeFlexCategoryTraits(index, ruler, options)
: computeFitCategoryTraits(index, ruler, options);

var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack);
var stackIndex = me.getStackIndex(me.index, me.getMeta().stack);
var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2);
var size = Math.min(
valueOrDefault(options.maxBarThickness, Infinity),
Expand All @@ -383,15 +440,13 @@ module.exports = DatasetController.extend({
var chart = me.chart;
var scale = me._getValueScale();
var rects = me.getMeta().data;
var dataset = me.getDataset();
var ilen = rects.length;
var i = 0;

helpers.canvas.clipArea(chart.ctx, chart.chartArea);

for (; i < ilen; ++i) {
var val = scale._parseValue(dataset.data[i]);
if (!isNaN(val.min) && !isNaN(val.max)) {
if (!isNaN(me._getParsed(i)[scale.id])) {
rects[i].draw();
}
}
Expand Down
36 changes: 27 additions & 9 deletions src/controllers/controller.bubble.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defaults._set('bubble', {
},
label: function(item, data) {
var datasetLabel = data.datasets[item.datasetIndex].label || '';
var dataPoint = data.datasets[item.datasetIndex].data[item.index];
var dataPoint = data.datasets[item.datasetIndex].data[item.index] || {r: '?'};
return datasetLabel + ': (' + item.label + ', ' + item.value + ', ' + dataPoint.r + ')';
}
}
Expand Down Expand Up @@ -59,6 +59,26 @@ module.exports = DatasetController.extend({
'rotation'
],

/**
* Parse array of objects
* @private
*/
_parseObjectData: function(meta, data, start, count) {
var xScale = this.getScaleForId(meta.xAxisID);
var yScale = this.getScaleForId(meta.yAxisID);
var parsed = [];
var i, ilen, item, obj;
for (i = start, ilen = start + count; i < ilen; ++i) {
obj = data[i];
item = {};
item[xScale.id] = xScale._parseObject(obj, 'x', i);
item[yScale.id] = yScale._parseObject(obj, 'y', i);
item._custom = obj && obj.r && +obj.r;
parsed.push(item);
}
return parsed;
},

/**
* @protected
*/
Expand All @@ -82,14 +102,12 @@ module.exports = DatasetController.extend({
var xScale = me.getScaleForId(meta.xAxisID);
var yScale = me.getScaleForId(meta.yAxisID);
var options = me._resolveDataElementOptions(index);
var data = me.getDataset().data[index];
var dsIndex = me.index;

var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(typeof data === 'object' ? data : NaN, index, dsIndex);
var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(data, index, dsIndex);
var parsed = !reset && me._getParsed(index);
var x = reset ? xScale.getPixelForDecimal(0.5) : xScale.getPixelForValue(parsed[xScale.id]);
var y = reset ? yScale.getBasePixel() : yScale.getPixelForValue(parsed[yScale.id]);

point._options = options;
point._datasetIndex = dsIndex;
point._datasetIndex = me.index;
point._index = index;
point._model = {
backgroundColor: options.backgroundColor,
Expand Down Expand Up @@ -135,7 +153,7 @@ module.exports = DatasetController.extend({
var me = this;
var chart = me.chart;
var dataset = me.getDataset();
var data = dataset.data[index] || {};
var parsed = me._getParsed(index);
var values = DatasetController.prototype._resolveDataElementOptions.apply(me, arguments);

// Scriptable options
Expand All @@ -153,7 +171,7 @@ module.exports = DatasetController.extend({

// Custom radius resolution
values.radius = resolve([
data.r,
parsed && parsed._custom,
me._config.radius,
chart.options.elements.point.radius
], context, index);
Expand Down
24 changes: 18 additions & 6 deletions src/controllers/controller.doughnut.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,19 @@ module.exports = DatasetController.extend({
'hoverBorderWidth',
],

/**
* Override data parsing, since we are not using scales
* @private
*/
_parse: function(start, count) {
var data = this.getDataset().data;
var metaData = this.getMeta().data;
var i, ilen;
for (i = start, ilen = start + count; i < ilen; ++i) {
metaData[i]._val = +data[i];
}
},

// Get index of the dataset in relation to the visible datasets. This allows determining the inner and outer radius correctly
getRingIndex: function(datasetIndex) {
var ringIndex = 0;
Expand Down Expand Up @@ -220,7 +233,7 @@ module.exports = DatasetController.extend({
var startAngle = opts.rotation; // non reset case handled later
var endAngle = opts.rotation; // non reset case handled later
var dataset = me.getDataset();
var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / DOUBLE_PI);
var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(arc._val * opts.circumference / DOUBLE_PI);
var innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius;
var outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius;
var options = arc._options || {};
Expand Down Expand Up @@ -264,14 +277,13 @@ module.exports = DatasetController.extend({
},

calculateTotal: function() {
var dataset = this.getDataset();
var meta = this.getMeta();
var metaData = this.getMeta().data;
var total = 0;
var value;

helpers.each(meta.data, function(element, index) {
value = dataset.data[index];
if (!isNaN(value) && !element.hidden) {
helpers.each(metaData, function(arc) {
value = arc ? arc._val : NaN;
if (!isNaN(value) && !arc.hidden) {
total += Math.abs(value);
}
});
Expand Down
Loading

0 comments on commit 72df272

Please sign in to comment.