mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
Refactored interaction modes to use lookup functions in Chart.Interaction.modes and added new modes for 'point', 'index', 'nearest', 'x', and 'y'
559 lines
17 KiB
JavaScript
559 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
module.exports = function(Chart) {
|
|
|
|
var helpers = Chart.helpers;
|
|
|
|
Chart.defaults.bar = {
|
|
hover: {
|
|
mode: 'label'
|
|
},
|
|
|
|
scales: {
|
|
xAxes: [{
|
|
type: 'category',
|
|
|
|
// Specific to Bar Controller
|
|
categoryPercentage: 0.8,
|
|
barPercentage: 0.9,
|
|
|
|
// grid line settings
|
|
gridLines: {
|
|
offsetGridLines: true
|
|
}
|
|
}],
|
|
yAxes: [{
|
|
type: 'linear'
|
|
}]
|
|
}
|
|
};
|
|
|
|
Chart.controllers.bar = Chart.DatasetController.extend({
|
|
|
|
dataElementType: Chart.elements.Rectangle,
|
|
|
|
initialize: function(chart, datasetIndex) {
|
|
Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex);
|
|
|
|
// Use this to indicate that this is a bar dataset.
|
|
this.getMeta().bar = true;
|
|
},
|
|
|
|
// Get the number of datasets that display bars. We use this to correctly calculate the bar width
|
|
getBarCount: function() {
|
|
var me = this;
|
|
var barCount = 0;
|
|
helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) {
|
|
var meta = me.chart.getDatasetMeta(datasetIndex);
|
|
if (meta.bar && me.chart.isDatasetVisible(datasetIndex)) {
|
|
++barCount;
|
|
}
|
|
}, me);
|
|
return barCount;
|
|
},
|
|
|
|
update: function(reset) {
|
|
var me = this;
|
|
helpers.each(me.getMeta().data, function(rectangle, index) {
|
|
me.updateElement(rectangle, index, reset);
|
|
}, me);
|
|
},
|
|
|
|
updateElement: function(rectangle, index, reset) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var xScale = me.getScaleForId(meta.xAxisID);
|
|
var yScale = me.getScaleForId(meta.yAxisID);
|
|
var scaleBase = yScale.getBasePixel();
|
|
var rectangleElementOptions = me.chart.options.elements.rectangle;
|
|
var custom = rectangle.custom || {};
|
|
var dataset = me.getDataset();
|
|
|
|
helpers.extend(rectangle, {
|
|
// Utility
|
|
_xScale: xScale,
|
|
_yScale: yScale,
|
|
_datasetIndex: me.index,
|
|
_index: index,
|
|
|
|
// Desired view properties
|
|
_model: {
|
|
x: me.calculateBarX(index, me.index),
|
|
y: reset ? scaleBase : me.calculateBarY(index, me.index),
|
|
|
|
// Tooltip
|
|
label: me.chart.data.labels[index],
|
|
datasetLabel: dataset.label,
|
|
|
|
// Appearance
|
|
base: reset ? scaleBase : me.calculateBarBase(me.index, index),
|
|
width: me.calculateBarWidth(index),
|
|
backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor),
|
|
borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped,
|
|
borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor),
|
|
borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth)
|
|
}
|
|
});
|
|
rectangle.pivot();
|
|
},
|
|
|
|
calculateBarBase: function(datasetIndex, index) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var yScale = me.getScaleForId(meta.yAxisID);
|
|
var base = 0;
|
|
|
|
if (yScale.options.stacked) {
|
|
var chart = me.chart;
|
|
var datasets = chart.data.datasets;
|
|
var value = Number(datasets[datasetIndex].data[index]);
|
|
|
|
for (var i = 0; i < datasetIndex; i++) {
|
|
var currentDs = datasets[i];
|
|
var currentDsMeta = chart.getDatasetMeta(i);
|
|
if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i)) {
|
|
var currentVal = Number(currentDs.data[index]);
|
|
base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0);
|
|
}
|
|
}
|
|
|
|
return yScale.getPixelForValue(base);
|
|
}
|
|
|
|
return yScale.getBasePixel();
|
|
},
|
|
|
|
getRuler: function(index) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var xScale = me.getScaleForId(meta.xAxisID);
|
|
var datasetCount = me.getBarCount();
|
|
|
|
var tickWidth;
|
|
|
|
if (xScale.options.type === 'category') {
|
|
tickWidth = xScale.getPixelForTick(index + 1) - xScale.getPixelForTick(index);
|
|
} else {
|
|
// Average width
|
|
tickWidth = xScale.width / xScale.ticks.length;
|
|
}
|
|
var categoryWidth = tickWidth * xScale.options.categoryPercentage;
|
|
var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2;
|
|
var fullBarWidth = categoryWidth / datasetCount;
|
|
|
|
if (xScale.ticks.length !== me.chart.data.labels.length) {
|
|
var perc = xScale.ticks.length / me.chart.data.labels.length;
|
|
fullBarWidth = fullBarWidth * perc;
|
|
}
|
|
|
|
var barWidth = fullBarWidth * xScale.options.barPercentage;
|
|
var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage);
|
|
|
|
return {
|
|
datasetCount: datasetCount,
|
|
tickWidth: tickWidth,
|
|
categoryWidth: categoryWidth,
|
|
categorySpacing: categorySpacing,
|
|
fullBarWidth: fullBarWidth,
|
|
barWidth: barWidth,
|
|
barSpacing: barSpacing
|
|
};
|
|
},
|
|
|
|
calculateBarWidth: function(index) {
|
|
var xScale = this.getScaleForId(this.getMeta().xAxisID);
|
|
if (xScale.options.barThickness) {
|
|
return xScale.options.barThickness;
|
|
}
|
|
var ruler = this.getRuler(index);
|
|
return xScale.options.stacked ? ruler.categoryWidth : ruler.barWidth;
|
|
},
|
|
|
|
// Get bar index from the given dataset index accounting for the fact that not all bars are visible
|
|
getBarIndex: function(datasetIndex) {
|
|
var barIndex = 0;
|
|
var meta, j;
|
|
|
|
for (j = 0; j < datasetIndex; ++j) {
|
|
meta = this.chart.getDatasetMeta(j);
|
|
if (meta.bar && this.chart.isDatasetVisible(j)) {
|
|
++barIndex;
|
|
}
|
|
}
|
|
|
|
return barIndex;
|
|
},
|
|
|
|
calculateBarX: function(index, datasetIndex) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var xScale = me.getScaleForId(meta.xAxisID);
|
|
var barIndex = me.getBarIndex(datasetIndex);
|
|
|
|
var ruler = me.getRuler(index);
|
|
var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo);
|
|
leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0;
|
|
|
|
if (xScale.options.stacked) {
|
|
return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing;
|
|
}
|
|
|
|
return leftTick +
|
|
(ruler.barWidth / 2) +
|
|
ruler.categorySpacing +
|
|
(ruler.barWidth * barIndex) +
|
|
(ruler.barSpacing / 2) +
|
|
(ruler.barSpacing * barIndex);
|
|
},
|
|
|
|
calculateBarY: function(index, datasetIndex) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var yScale = me.getScaleForId(meta.yAxisID);
|
|
var value = Number(me.getDataset().data[index]);
|
|
|
|
if (yScale.options.stacked) {
|
|
|
|
var sumPos = 0,
|
|
sumNeg = 0;
|
|
|
|
for (var i = 0; i < datasetIndex; i++) {
|
|
var ds = me.chart.data.datasets[i];
|
|
var dsMeta = me.chart.getDatasetMeta(i);
|
|
if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i)) {
|
|
var stackedVal = Number(ds.data[index]);
|
|
if (stackedVal < 0) {
|
|
sumNeg += stackedVal || 0;
|
|
} else {
|
|
sumPos += stackedVal || 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (value < 0) {
|
|
return yScale.getPixelForValue(sumNeg + value);
|
|
}
|
|
return yScale.getPixelForValue(sumPos + value);
|
|
}
|
|
|
|
return yScale.getPixelForValue(value);
|
|
},
|
|
|
|
draw: function(ease) {
|
|
var me = this;
|
|
var easingDecimal = ease || 1;
|
|
helpers.each(me.getMeta().data, function(rectangle, index) {
|
|
var d = me.getDataset().data[index];
|
|
if (d !== null && d !== undefined && !isNaN(d)) {
|
|
rectangle.transition(easingDecimal).draw();
|
|
}
|
|
}, me);
|
|
},
|
|
|
|
setHoverStyle: function(rectangle) {
|
|
var dataset = this.chart.data.datasets[rectangle._datasetIndex];
|
|
var index = rectangle._index;
|
|
|
|
var custom = rectangle.custom || {};
|
|
var model = rectangle._model;
|
|
model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor));
|
|
model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.getHoverColor(model.borderColor));
|
|
model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, model.borderWidth);
|
|
},
|
|
|
|
removeHoverStyle: function(rectangle) {
|
|
var dataset = this.chart.data.datasets[rectangle._datasetIndex];
|
|
var index = rectangle._index;
|
|
var custom = rectangle.custom || {};
|
|
var model = rectangle._model;
|
|
var rectangleElementOptions = this.chart.options.elements.rectangle;
|
|
|
|
model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor);
|
|
model.borderColor = custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor);
|
|
model.borderWidth = custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth);
|
|
}
|
|
|
|
});
|
|
|
|
|
|
// including horizontalBar in the bar file, instead of a file of its own
|
|
// it extends bar (like pie extends doughnut)
|
|
Chart.defaults.horizontalBar = {
|
|
hover: {
|
|
mode: 'label'
|
|
},
|
|
|
|
scales: {
|
|
xAxes: [{
|
|
type: 'linear',
|
|
position: 'bottom'
|
|
}],
|
|
yAxes: [{
|
|
position: 'left',
|
|
type: 'category',
|
|
|
|
// Specific to Horizontal Bar Controller
|
|
categoryPercentage: 0.8,
|
|
barPercentage: 0.9,
|
|
|
|
// grid line settings
|
|
gridLines: {
|
|
offsetGridLines: true
|
|
}
|
|
}]
|
|
},
|
|
elements: {
|
|
rectangle: {
|
|
borderSkipped: 'left'
|
|
}
|
|
},
|
|
tooltips: {
|
|
callbacks: {
|
|
title: function(tooltipItems, data) {
|
|
// Pick first xLabel for now
|
|
var title = '';
|
|
|
|
if (tooltipItems.length > 0) {
|
|
if (tooltipItems[0].yLabel) {
|
|
title = tooltipItems[0].yLabel;
|
|
} else if (data.labels.length > 0 && tooltipItems[0].index < data.labels.length) {
|
|
title = data.labels[tooltipItems[0].index];
|
|
}
|
|
}
|
|
|
|
return title;
|
|
},
|
|
label: function(tooltipItem, data) {
|
|
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
return datasetLabel + ': ' + tooltipItem.xLabel;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Chart.controllers.horizontalBar = Chart.controllers.bar.extend({
|
|
updateElement: function(rectangle, index, reset) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var xScale = me.getScaleForId(meta.xAxisID);
|
|
var yScale = me.getScaleForId(meta.yAxisID);
|
|
var scaleBase = xScale.getBasePixel();
|
|
var custom = rectangle.custom || {};
|
|
var dataset = me.getDataset();
|
|
var rectangleElementOptions = me.chart.options.elements.rectangle;
|
|
|
|
helpers.extend(rectangle, {
|
|
// Utility
|
|
_xScale: xScale,
|
|
_yScale: yScale,
|
|
_datasetIndex: me.index,
|
|
_index: index,
|
|
|
|
// Desired view properties
|
|
_model: {
|
|
x: reset ? scaleBase : me.calculateBarX(index, me.index),
|
|
y: me.calculateBarY(index, me.index),
|
|
|
|
// Tooltip
|
|
label: me.chart.data.labels[index],
|
|
datasetLabel: dataset.label,
|
|
|
|
// Appearance
|
|
base: reset ? scaleBase : me.calculateBarBase(me.index, index),
|
|
height: me.calculateBarHeight(index),
|
|
backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor),
|
|
borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped,
|
|
borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor),
|
|
borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth)
|
|
},
|
|
|
|
draw: function() {
|
|
var ctx = this._chart.ctx;
|
|
var vm = this._view;
|
|
|
|
var halfHeight = vm.height / 2,
|
|
topY = vm.y - halfHeight,
|
|
bottomY = vm.y + halfHeight,
|
|
right = vm.base - (vm.base - vm.x),
|
|
halfStroke = vm.borderWidth / 2;
|
|
|
|
// Canvas doesn't allow us to stroke inside the width so we can
|
|
// adjust the sizes to fit if we're setting a stroke on the line
|
|
if (vm.borderWidth) {
|
|
topY += halfStroke;
|
|
bottomY -= halfStroke;
|
|
right += halfStroke;
|
|
}
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.fillStyle = vm.backgroundColor;
|
|
ctx.strokeStyle = vm.borderColor;
|
|
ctx.lineWidth = vm.borderWidth;
|
|
|
|
// Corner points, from bottom-left to bottom-right clockwise
|
|
// | 1 2 |
|
|
// | 0 3 |
|
|
var corners = [
|
|
[vm.base, bottomY],
|
|
[vm.base, topY],
|
|
[right, topY],
|
|
[right, bottomY]
|
|
];
|
|
|
|
// Find first (starting) corner with fallback to 'bottom'
|
|
var borders = ['bottom', 'left', 'top', 'right'];
|
|
var startCorner = borders.indexOf(vm.borderSkipped, 0);
|
|
if (startCorner === -1) {
|
|
startCorner = 0;
|
|
}
|
|
|
|
function cornerAt(cornerIndex) {
|
|
return corners[(startCorner + cornerIndex) % 4];
|
|
}
|
|
|
|
// Draw rectangle from 'startCorner'
|
|
ctx.moveTo.apply(ctx, cornerAt(0));
|
|
for (var i = 1; i < 4; i++) {
|
|
ctx.lineTo.apply(ctx, cornerAt(i));
|
|
}
|
|
|
|
ctx.fill();
|
|
if (vm.borderWidth) {
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
});
|
|
|
|
rectangle.pivot();
|
|
},
|
|
|
|
calculateBarBase: function(datasetIndex, index) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var xScale = me.getScaleForId(meta.xAxisID);
|
|
var base = 0;
|
|
|
|
if (xScale.options.stacked) {
|
|
var chart = me.chart;
|
|
var datasets = chart.data.datasets;
|
|
var value = Number(datasets[datasetIndex].data[index]);
|
|
|
|
for (var i = 0; i < datasetIndex; i++) {
|
|
var currentDs = datasets[i];
|
|
var currentDsMeta = chart.getDatasetMeta(i);
|
|
if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i)) {
|
|
var currentVal = Number(currentDs.data[index]);
|
|
base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0);
|
|
}
|
|
}
|
|
|
|
return xScale.getPixelForValue(base);
|
|
}
|
|
|
|
return xScale.getBasePixel();
|
|
},
|
|
|
|
getRuler: function(index) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var yScale = me.getScaleForId(meta.yAxisID);
|
|
var datasetCount = me.getBarCount();
|
|
|
|
var tickHeight;
|
|
if (yScale.options.type === 'category') {
|
|
tickHeight = yScale.getPixelForTick(index + 1) - yScale.getPixelForTick(index);
|
|
} else {
|
|
// Average width
|
|
tickHeight = yScale.width / yScale.ticks.length;
|
|
}
|
|
var categoryHeight = tickHeight * yScale.options.categoryPercentage;
|
|
var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2;
|
|
var fullBarHeight = categoryHeight / datasetCount;
|
|
|
|
if (yScale.ticks.length !== me.chart.data.labels.length) {
|
|
var perc = yScale.ticks.length / me.chart.data.labels.length;
|
|
fullBarHeight = fullBarHeight * perc;
|
|
}
|
|
|
|
var barHeight = fullBarHeight * yScale.options.barPercentage;
|
|
var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage);
|
|
|
|
return {
|
|
datasetCount: datasetCount,
|
|
tickHeight: tickHeight,
|
|
categoryHeight: categoryHeight,
|
|
categorySpacing: categorySpacing,
|
|
fullBarHeight: fullBarHeight,
|
|
barHeight: barHeight,
|
|
barSpacing: barSpacing
|
|
};
|
|
},
|
|
|
|
calculateBarHeight: function(index) {
|
|
var me = this;
|
|
var yScale = me.getScaleForId(me.getMeta().yAxisID);
|
|
if (yScale.options.barThickness) {
|
|
return yScale.options.barThickness;
|
|
}
|
|
var ruler = me.getRuler(index);
|
|
return yScale.options.stacked ? ruler.categoryHeight : ruler.barHeight;
|
|
},
|
|
|
|
calculateBarX: function(index, datasetIndex) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var xScale = me.getScaleForId(meta.xAxisID);
|
|
var value = Number(me.getDataset().data[index]);
|
|
|
|
if (xScale.options.stacked) {
|
|
|
|
var sumPos = 0,
|
|
sumNeg = 0;
|
|
|
|
for (var i = 0; i < datasetIndex; i++) {
|
|
var ds = me.chart.data.datasets[i];
|
|
var dsMeta = me.chart.getDatasetMeta(i);
|
|
if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i)) {
|
|
var stackedVal = Number(ds.data[index]);
|
|
if (stackedVal < 0) {
|
|
sumNeg += stackedVal || 0;
|
|
} else {
|
|
sumPos += stackedVal || 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (value < 0) {
|
|
return xScale.getPixelForValue(sumNeg + value);
|
|
}
|
|
return xScale.getPixelForValue(sumPos + value);
|
|
}
|
|
|
|
return xScale.getPixelForValue(value);
|
|
},
|
|
|
|
calculateBarY: function(index, datasetIndex) {
|
|
var me = this;
|
|
var meta = me.getMeta();
|
|
var yScale = me.getScaleForId(meta.yAxisID);
|
|
var barIndex = me.getBarIndex(datasetIndex);
|
|
|
|
var ruler = me.getRuler(index);
|
|
var topTick = yScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo);
|
|
topTick -= me.chart.isCombo ? (ruler.tickHeight / 2) : 0;
|
|
|
|
if (yScale.options.stacked) {
|
|
return topTick + (ruler.categoryHeight / 2) + ruler.categorySpacing;
|
|
}
|
|
|
|
return topTick +
|
|
(ruler.barHeight / 2) +
|
|
ruler.categorySpacing +
|
|
(ruler.barHeight * barIndex) +
|
|
(ruler.barSpacing / 2) +
|
|
(ruler.barSpacing * barIndex);
|
|
}
|
|
});
|
|
};
|