chart.js 2 を中心に構築された react-chartjs-2 を使用しています。Chartjs はバージョン 3 を大幅に変更してロールアウトしましたが、react-chartjs はまだ chartjs-3 のラッパーを開発していません。
グループ化された棒グラフで null 値をスキップしたいのですが、それ以外の場合は空白が残ります。null 値をスキップするオプションが chartjs-3 に追加されましたが、chartjs-2 にはありません。chartjs-2 を使用すると、プロットは以下のようになり、null 値のギャップがあります。
したがって、このコミット変更#7849を使用し、chartjs-2.9.2 にも同じ変更を加えています。バーの幅は、null 値の領域を埋めるために増加しましたが、右に移動し、以下のように重なっています。なぜこのように振る舞うのですか?どんな助けでも大歓迎です。
以下のcontoller.bar.jsコードを変更しました
"use strict";
var DatasetController = require("../core/core.datasetController");
var defaults = require("../core/core.defaults");
var elements = require("../elements/index");
var helpers = require("../helpers/index");
var deprecated = helpers._deprecated;
var valueOrDefault = helpers.valueOrDefault;
defaults._set("bar", {
hover: {
mode: "label",
},
scales: {
xAxes: [
{
type: "category",
offset: true,
gridLines: {
offsetGridLines: true,
},
},
],
yAxes: [
{
type: "linear",
},
],
},
});
defaults._set("global", {
datasets: {
bar: {
categoryPercentage: 0.8,
barPercentage: 0.9,
},
},
});
/**
* Computes the "optimal" sample size to maintain bars equally sized while preventing overlap.
* @private
*/
function computeMinSampleSize(scale, pixels) {
var min = scale._length;
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 = scale.getTicks().length; i < ilen; ++i) {
curr = scale.getPixelForTick(i);
min = i > 0 ? Math.min(min, Math.abs(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, stackCount) {
var thickness = options.barThickness;
var curr = ruler.pixels[index];
var min = helpers.isNullOrUndef(thickness)
? computeMinSampleSize(ruler.scale, ruler.pixels)
: -1;
var size, ratio;
if (helpers.isNullOrUndef(thickness)) {
size = 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 * stackCount;
ratio = 1;
}
return {
chunk: size / stackCount,
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, stackCount) {
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 / stackCount,
ratio: options.barPercentage,
start: start,
};
}
module.exports = DatasetController.extend({
dataElementType: elements.Rectangle,
/**
* @private
*/
_dataElementOptions: [
"backgroundColor",
"borderColor",
"borderSkipped",
"borderWidth",
"barPercentage",
"barThickness",
"categoryPercentage",
"maxBarThickness",
"minBarLength",
],
initialize: function () {
var me = this;
var meta, scaleOpts;
DatasetController.prototype.initialize.apply(me, arguments);
meta = me.getMeta();
meta.stack = me.getDataset().stack;
meta.bar = true;
scaleOpts = me._getIndexScale().options;
deprecated(
"bar chart",
scaleOpts.barPercentage,
"scales.[x/y]Axes.barPercentage",
"dataset.barPercentage"
);
deprecated(
"bar chart",
scaleOpts.barThickness,
"scales.[x/y]Axes.barThickness",
"dataset.barThickness"
);
deprecated(
"bar chart",
scaleOpts.categoryPercentage,
"scales.[x/y]Axes.categoryPercentage",
"dataset.categoryPercentage"
);
deprecated(
"bar chart",
me._getValueScale().options.minBarLength,
"scales.[x/y]Axes.minBarLength",
"dataset.minBarLength"
);
deprecated(
"bar chart",
scaleOpts.maxBarThickness,
"scales.[x/y]Axes.maxBarThickness",
"dataset.maxBarThickness"
);
},
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, options);
rectangle.pivot();
},
/**
* @private
*/
_updateElementGeometry: function (rectangle, index, reset, options) {
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, options);
var ipixels = me.calculateBarIndexPixels(
me.index,
index,
ruler,
options
);
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, dataIndex) {
var me = this;
var scale = me._getIndexScale();
var metasets = scale._getMatchingVisibleMetas(me._type);
var stacked = scale.options.stacked;
var ilen = metasets.length;
var stacks = [];
var i, meta;
for (i = 0; i < ilen; ++i) {
meta = metasets[i];
if (
typeof dataIndex !== "undefined" &&
helpers.isNullOrUndef(
me.chart.data.datasets[meta.index].data[dataIndex]
)
) {
continue;
}
// stacked | meta.stack
// | found | not found | undefined
// false | x | x | x
// true | | x |
// undefined | | x | x
if (
stacked === false ||
stacks.indexOf(meta.stack) === -1 ||
(stacked === undefined && meta.stack === undefined)
) {
stacks.push(meta.stack);
}
if (meta.index === last) {
break;
}
}
return stacks;
},
/**
* Returns the effective number of stacks based on groups and bar visibility.
* @private
*/
getStackCount: function (index) {
return this._getStacks(undefined, index).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 pixels = [];
var i, ilen;
for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) {
pixels.push(scale.getPixelForValue(null, i, me.index));
}
return {
pixels: pixels,
start: scale._startPixel,
end: scale._endPixel,
stackCount: me.getStackCount(),
scale: scale,
};
},
/**
* Note: pixel values are not clamped to the scale area.
* @private
*/
calculateBarValuePixels: function (datasetIndex, 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 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;
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, options) {
var me = this;
const stackCount = true ? me.getStackCount(index) : ruler.stackCount;
var range =
options.barThickness === "flex"
? computeFlexCategoryTraits(index, ruler, options, stackCount)
: computeFitCategoryTraits(index, ruler, options, stackCount);
var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack);
var center = range.start + range.chunk * stackIndex + range.chunk / 2;
console.log(datasetIndex, index, stackIndex, center, range);
var size = Math.min(
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);
},
/**
* @private
*/
_resolveDataElementOptions: function () {
var me = this;
var values = helpers.extend(
{},
DatasetController.prototype._resolveDataElementOptions.apply(
me,
arguments
)
);
var indexOpts = me._getIndexScale().options;
var valueOpts = me._getValueScale().options;
values.barPercentage = valueOrDefault(
indexOpts.barPercentage,
values.barPercentage
);
values.barThickness = valueOrDefault(
indexOpts.barThickness,
values.barThickness
);
values.categoryPercentage = valueOrDefault(
indexOpts.categoryPercentage,
values.categoryPercentage
);
values.maxBarThickness = valueOrDefault(
indexOpts.maxBarThickness,
values.maxBarThickness
);
values.minBarLength = valueOrDefault(
valueOpts.minBarLength,
values.minBarLength
);
return values;
},
});