mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
393 lines
10 KiB
JavaScript
393 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
var DatasetController = require('../core/core.datasetController');
|
|
var defaults = require('../core/core.defaults');
|
|
var elements = require('../elements/index');
|
|
var helpers = require('../helpers/index');
|
|
|
|
defaults._set('bar', {
|
|
hover: {
|
|
mode: 'label'
|
|
},
|
|
|
|
scales: {
|
|
xAxes: [{
|
|
type: 'category',
|
|
categoryPercentage: 0.8,
|
|
barPercentage: 0.9,
|
|
offset: true,
|
|
gridLines: {
|
|
offsetGridLines: true
|
|
}
|
|
}],
|
|
|
|
yAxes: [{
|
|
type: 'linear'
|
|
}]
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Computes the "optimal" sample size to maintain bars equally sized while preventing overlap.
|
|
* @private
|
|
*/
|
|
function computeMinSampleSize(scale, pixels) {
|
|
var min = scale.isHorizontal() ? scale.width : scale.height;
|
|
var ticks = scale.getTicks();
|
|
var prev, curr, i, ilen;
|
|
|
|
for (i = 1, ilen = pixels.length; i < ilen; ++i) {
|
|
min = Math.min(min, Math.abs(pixels[i] - pixels[i - 1]));
|
|
}
|
|
|
|
for (i = 0, ilen = ticks.length; i < ilen; ++i) {
|
|
curr = scale.getPixelForTick(i);
|
|
min = i > 0 ? Math.min(min, curr - prev) : min;
|
|
prev = curr;
|
|
}
|
|
|
|
return min;
|
|
}
|
|
|
|
/**
|
|
* Computes an "ideal" category based on the absolute bar thickness or, if undefined or null,
|
|
* uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This
|
|
* mode currently always generates bars equally sized (until we introduce scriptable options?).
|
|
* @private
|
|
*/
|
|
function computeFitCategoryTraits(index, ruler, options) {
|
|
var thickness = options.barThickness;
|
|
var count = ruler.stackCount;
|
|
var curr = ruler.pixels[index];
|
|
var size, ratio;
|
|
|
|
if (helpers.isNullOrUndef(thickness)) {
|
|
size = ruler.min * options.categoryPercentage;
|
|
ratio = options.barPercentage;
|
|
} else {
|
|
// When bar thickness is enforced, category and bar percentages are ignored.
|
|
// Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%')
|
|
// and deprecate barPercentage since this value is ignored when thickness is absolute.
|
|
size = thickness * count;
|
|
ratio = 1;
|
|
}
|
|
|
|
return {
|
|
chunk: size / count,
|
|
ratio: ratio,
|
|
start: curr - (size / 2)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Computes an "optimal" category that globally arranges bars side by side (no gap when
|
|
* percentage options are 1), based on the previous and following categories. This mode
|
|
* generates bars with different widths when data are not evenly spaced.
|
|
* @private
|
|
*/
|
|
function computeFlexCategoryTraits(index, ruler, options) {
|
|
var pixels = ruler.pixels;
|
|
var curr = pixels[index];
|
|
var prev = index > 0 ? pixels[index - 1] : null;
|
|
var next = index < pixels.length - 1 ? pixels[index + 1] : null;
|
|
var percent = options.categoryPercentage;
|
|
var start, size;
|
|
|
|
if (prev === null) {
|
|
// first data: its size is double based on the next point or,
|
|
// if it's also the last data, we use the scale size.
|
|
prev = curr - (next === null ? ruler.end - ruler.start : next - curr);
|
|
}
|
|
|
|
if (next === null) {
|
|
// last data: its size is also double based on the previous point.
|
|
next = curr + curr - prev;
|
|
}
|
|
|
|
start = curr - (curr - Math.min(prev, next)) / 2 * percent;
|
|
size = Math.abs(next - prev) / 2 * percent;
|
|
|
|
return {
|
|
chunk: size / ruler.stackCount,
|
|
ratio: options.barPercentage,
|
|
start: start
|
|
};
|
|
}
|
|
|
|
module.exports = DatasetController.extend({
|
|
|
|
dataElementType: elements.Rectangle,
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_dataElementOptions: [
|
|
'backgroundColor',
|
|
'borderColor',
|
|
'borderSkipped',
|
|
'borderWidth'
|
|
],
|
|
|
|
initialize: function() {
|
|
var me = this;
|
|
var meta;
|
|
|
|
DatasetController.prototype.initialize.apply(me, arguments);
|
|
|
|
meta = me.getMeta();
|
|
meta.stack = me.getDataset().stack;
|
|
meta.bar = true;
|
|
},
|
|
|
|
update: function(reset) {
|
|
var me = this;
|
|
var rects = me.getMeta().data;
|
|
var i, ilen;
|
|
|
|
me._ruler = me.getRuler();
|
|
|
|
for (i = 0, ilen = rects.length; i < ilen; ++i) {
|
|
me.updateElement(rects[i], i, reset);
|
|
}
|
|
},
|
|
|
|
updateElement: function(rectangle, index, reset) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var dataset = me.getDataset();
|
|
var options = me._resolveDataElementOptions(rectangle, index);
|
|
|
|
rectangle._xScale = me.getScaleForId(meta.xAxisID);
|
|
rectangle._yScale = me.getScaleForId(meta.yAxisID);
|
|
rectangle._datasetIndex = me.index;
|
|
rectangle._index = index;
|
|
rectangle._model = {
|
|
backgroundColor: options.backgroundColor,
|
|
borderColor: options.borderColor,
|
|
borderSkipped: options.borderSkipped,
|
|
borderWidth: options.borderWidth,
|
|
datasetLabel: dataset.label,
|
|
label: me.chart.data.labels[index]
|
|
};
|
|
|
|
if (helpers.isArray(dataset.data[index])) {
|
|
rectangle._model.borderSkipped = null;
|
|
}
|
|
|
|
me._updateElementGeometry(rectangle, index, reset);
|
|
|
|
rectangle.pivot();
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_updateElementGeometry: function(rectangle, index, reset) {
|
|
var me = this;
|
|
var model = rectangle._model;
|
|
var vscale = me._getValueScale();
|
|
var base = vscale.getBasePixel();
|
|
var horizontal = vscale.isHorizontal();
|
|
var ruler = me._ruler || me.getRuler();
|
|
var vpixels = me.calculateBarValuePixels(me.index, index);
|
|
var ipixels = me.calculateBarIndexPixels(me.index, index, ruler);
|
|
|
|
model.horizontal = horizontal;
|
|
model.base = reset ? base : vpixels.base;
|
|
model.x = horizontal ? reset ? base : vpixels.head : ipixels.center;
|
|
model.y = horizontal ? ipixels.center : reset ? base : vpixels.head;
|
|
model.height = horizontal ? ipixels.size : undefined;
|
|
model.width = horizontal ? undefined : ipixels.size;
|
|
},
|
|
|
|
/**
|
|
* Returns the stacks based on groups and bar visibility.
|
|
* @param {number} [last] - The dataset index
|
|
* @returns {string[]} The list of stack IDs
|
|
* @private
|
|
*/
|
|
_getStacks: function(last) {
|
|
var me = this;
|
|
var chart = me.chart;
|
|
var scale = me._getIndexScale();
|
|
var stacked = scale.options.stacked;
|
|
var ilen = last === undefined ? chart.data.datasets.length : last + 1;
|
|
var stacks = [];
|
|
var i, meta;
|
|
|
|
for (i = 0; i < ilen; ++i) {
|
|
meta = chart.getDatasetMeta(i);
|
|
if (meta.bar && chart.isDatasetVisible(i) &&
|
|
(stacked === false ||
|
|
(stacked === true && stacks.indexOf(meta.stack) === -1) ||
|
|
(stacked === undefined && (meta.stack === undefined || stacks.indexOf(meta.stack) === -1)))) {
|
|
stacks.push(meta.stack);
|
|
}
|
|
}
|
|
|
|
return stacks;
|
|
},
|
|
|
|
/**
|
|
* Returns the effective number of stacks based on groups and bar visibility.
|
|
* @private
|
|
*/
|
|
getStackCount: function() {
|
|
return this._getStacks().length;
|
|
},
|
|
|
|
/**
|
|
* Returns the stack index for the given dataset based on groups and bar visibility.
|
|
* @param {number} [datasetIndex] - The dataset index
|
|
* @param {string} [name] - The stack name to find
|
|
* @returns {number} The stack index
|
|
* @private
|
|
*/
|
|
getStackIndex: function(datasetIndex, name) {
|
|
var stacks = this._getStacks(datasetIndex);
|
|
var index = (name !== undefined)
|
|
? stacks.indexOf(name)
|
|
: -1; // indexOf returns -1 if element is not present
|
|
|
|
return (index === -1)
|
|
? stacks.length - 1
|
|
: index;
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
getRuler: function() {
|
|
var me = this;
|
|
var scale = me._getIndexScale();
|
|
var stackCount = me.getStackCount();
|
|
var datasetIndex = me.index;
|
|
var isHorizontal = scale.isHorizontal();
|
|
var start = isHorizontal ? scale.left : scale.top;
|
|
var end = start + (isHorizontal ? scale.width : scale.height);
|
|
var pixels = [];
|
|
var i, ilen, min;
|
|
|
|
for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) {
|
|
pixels.push(scale.getPixelForValue(null, i, datasetIndex));
|
|
}
|
|
|
|
min = helpers.isNullOrUndef(scale.options.barThickness)
|
|
? computeMinSampleSize(scale, pixels)
|
|
: -1;
|
|
|
|
return {
|
|
min: min,
|
|
pixels: pixels,
|
|
start: start,
|
|
end: end,
|
|
stackCount: stackCount,
|
|
scale: scale
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Note: pixel values are not clamped to the scale area.
|
|
* @private
|
|
*/
|
|
calculateBarValuePixels: function(datasetIndex, index) {
|
|
var me = this;
|
|
var chart = me.chart;
|
|
var meta = me.getMeta();
|
|
var scale = me._getValueScale();
|
|
var isHorizontal = scale.isHorizontal();
|
|
var datasets = chart.data.datasets;
|
|
var value = scale._parseValue(datasets[datasetIndex].data[index]);
|
|
var minBarLength = scale.options.minBarLength;
|
|
var stacked = scale.options.stacked;
|
|
var stack = meta.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 i, imeta, ivalue, base, head, size, stackLength;
|
|
|
|
if (stacked || (stacked === undefined && stack !== undefined)) {
|
|
for (i = 0; i < datasetIndex; ++i) {
|
|
imeta = chart.getDatasetMeta(i);
|
|
|
|
if (imeta.bar &&
|
|
imeta.stack === stack &&
|
|
imeta.controller._getValueScaleId() === scale.id &&
|
|
chart.isDatasetVisible(i)) {
|
|
|
|
stackLength = scale._parseValue(datasets[i].data[index]);
|
|
ivalue = stackLength.start === undefined ? stackLength.end : stackLength.min >= 0 && stackLength.max >= 0 ? stackLength.max : stackLength.min;
|
|
|
|
if ((value.min < 0 && ivalue < 0) || (value.max >= 0 && ivalue > 0)) {
|
|
start += ivalue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
base = scale.getPixelForValue(start);
|
|
head = scale.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;
|
|
}
|
|
}
|
|
|
|
return {
|
|
size: size,
|
|
base: base,
|
|
head: head,
|
|
center: head + size / 2
|
|
};
|
|
},
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
calculateBarIndexPixels: function(datasetIndex, index, ruler) {
|
|
var me = this;
|
|
var options = ruler.scale.options;
|
|
var range = options.barThickness === 'flex'
|
|
? computeFlexCategoryTraits(index, ruler, options)
|
|
: computeFitCategoryTraits(index, ruler, options);
|
|
|
|
var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack);
|
|
var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2);
|
|
var size = Math.min(
|
|
helpers.valueOrDefault(options.maxBarThickness, Infinity),
|
|
range.chunk * range.ratio);
|
|
|
|
return {
|
|
base: center - size / 2,
|
|
head: center + size / 2,
|
|
center: center,
|
|
size: size
|
|
};
|
|
},
|
|
|
|
draw: function() {
|
|
var me = this;
|
|
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)) {
|
|
rects[i].draw();
|
|
}
|
|
}
|
|
|
|
helpers.canvas.unclipArea(chart.ctx);
|
|
}
|
|
});
|