Actualización

This commit is contained in:
Xes
2025-04-10 11:37:29 +02:00
parent 4bfeadb360
commit 8969cc929d
39112 changed files with 975884 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
'use strict';
var category = require('./scale.category');
var linear = require('./scale.linear');
var logarithmic = require('./scale.logarithmic');
var radialLinear = require('./scale.radialLinear');
var time = require('./scale.time');
module.exports = {
category: category,
linear: linear,
logarithmic: logarithmic,
radialLinear: radialLinear,
time: time
};

View File

@@ -0,0 +1,131 @@
'use strict';
var helpers = require('../helpers/index');
var Scale = require('../core/core.scale');
var isNullOrUndef = helpers.isNullOrUndef;
var defaultConfig = {
position: 'bottom'
};
module.exports = Scale.extend({
determineDataLimits: function() {
var me = this;
var labels = me._getLabels();
var ticksOpts = me.options.ticks;
var min = ticksOpts.min;
var max = ticksOpts.max;
var minIndex = 0;
var maxIndex = labels.length - 1;
var findIndex;
if (min !== undefined) {
// user specified min value
findIndex = labels.indexOf(min);
if (findIndex >= 0) {
minIndex = findIndex;
}
}
if (max !== undefined) {
// user specified max value
findIndex = labels.indexOf(max);
if (findIndex >= 0) {
maxIndex = findIndex;
}
}
me.minIndex = minIndex;
me.maxIndex = maxIndex;
me.min = labels[minIndex];
me.max = labels[maxIndex];
},
buildTicks: function() {
var me = this;
var labels = me._getLabels();
var minIndex = me.minIndex;
var maxIndex = me.maxIndex;
// If we are viewing some subset of labels, slice the original array
me.ticks = (minIndex === 0 && maxIndex === labels.length - 1) ? labels : labels.slice(minIndex, maxIndex + 1);
},
getLabelForIndex: function(index, datasetIndex) {
var me = this;
var chart = me.chart;
if (chart.getDatasetMeta(datasetIndex).controller._getValueScaleId() === me.id) {
return me.getRightValue(chart.data.datasets[datasetIndex].data[index]);
}
return me._getLabels()[index];
},
_configure: function() {
var me = this;
var offset = me.options.offset;
var ticks = me.ticks;
Scale.prototype._configure.call(me);
if (!me.isHorizontal()) {
// For backward compatibility, vertical category scale reverse is inverted.
me._reversePixels = !me._reversePixels;
}
if (!ticks) {
return;
}
me._startValue = me.minIndex - (offset ? 0.5 : 0);
me._valueRange = Math.max(ticks.length - (offset ? 0 : 1), 1);
},
// Used to get data value locations. Value can either be an index or a numerical value
getPixelForValue: function(value, index, datasetIndex) {
var me = this;
var valueCategory, labels, idx;
if (!isNullOrUndef(index) && !isNullOrUndef(datasetIndex)) {
value = me.chart.data.datasets[datasetIndex].data[index];
}
// If value is a data object, then index is the index in the data array,
// not the index of the scale. We need to change that.
if (!isNullOrUndef(value)) {
valueCategory = me.isHorizontal() ? value.x : value.y;
}
if (valueCategory !== undefined || (value !== undefined && isNaN(index))) {
labels = me._getLabels();
value = helpers.valueOrDefault(valueCategory, value);
idx = labels.indexOf(value);
index = idx !== -1 ? idx : index;
if (isNaN(index)) {
index = value;
}
}
return me.getPixelForDecimal((index - me._startValue) / me._valueRange);
},
getPixelForTick: function(index) {
var ticks = this.ticks;
return index < 0 || index > ticks.length - 1
? null
: this.getPixelForValue(ticks[index], index + this.minIndex);
},
getValueForPixel: function(pixel) {
var me = this;
var value = Math.round(me._startValue + me.getDecimalForPixel(pixel) * me._valueRange);
return Math.min(Math.max(value, 0), me.ticks.length - 1);
},
getBasePixel: function() {
return this.bottom;
}
});
// INTERNAL: static default options, registered in src/index.js
module.exports._defaults = defaultConfig;

View File

@@ -0,0 +1,167 @@
'use strict';
var helpers = require('../helpers/index');
var LinearScaleBase = require('./scale.linearbase');
var Ticks = require('../core/core.ticks');
var defaultConfig = {
position: 'left',
ticks: {
callback: Ticks.formatters.linear
}
};
var DEFAULT_MIN = 0;
var DEFAULT_MAX = 1;
function getOrCreateStack(stacks, stacked, meta) {
var key = [
meta.type,
// we have a separate stack for stack=undefined datasets when the opts.stacked is undefined
stacked === undefined && meta.stack === undefined ? meta.index : '',
meta.stack
].join('.');
if (stacks[key] === undefined) {
stacks[key] = {
pos: [],
neg: []
};
}
return stacks[key];
}
function stackData(scale, stacks, meta, data) {
var opts = scale.options;
var stacked = opts.stacked;
var stack = getOrCreateStack(stacks, stacked, meta);
var pos = stack.pos;
var neg = stack.neg;
var ilen = data.length;
var i, value;
for (i = 0; i < ilen; ++i) {
value = scale._parseValue(data[i]);
if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden) {
continue;
}
pos[i] = pos[i] || 0;
neg[i] = neg[i] || 0;
if (opts.relativePoints) {
pos[i] = 100;
} else if (value.min < 0 || value.max < 0) {
neg[i] += value.min;
} else {
pos[i] += value.max;
}
}
}
function updateMinMax(scale, meta, data) {
var ilen = data.length;
var i, value;
for (i = 0; i < ilen; ++i) {
value = scale._parseValue(data[i]);
if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden) {
continue;
}
scale.min = Math.min(scale.min, value.min);
scale.max = Math.max(scale.max, value.max);
}
}
module.exports = LinearScaleBase.extend({
determineDataLimits: function() {
var me = this;
var opts = me.options;
var chart = me.chart;
var datasets = chart.data.datasets;
var metasets = me._getMatchingVisibleMetas();
var hasStacks = opts.stacked;
var stacks = {};
var ilen = metasets.length;
var i, meta, data, values;
me.min = Number.POSITIVE_INFINITY;
me.max = Number.NEGATIVE_INFINITY;
if (hasStacks === undefined) {
for (i = 0; !hasStacks && i < ilen; ++i) {
meta = metasets[i];
hasStacks = meta.stack !== undefined;
}
}
for (i = 0; i < ilen; ++i) {
meta = metasets[i];
data = datasets[meta.index].data;
if (hasStacks) {
stackData(me, stacks, meta, data);
} else {
updateMinMax(me, meta, data);
}
}
helpers.each(stacks, function(stackValues) {
values = stackValues.pos.concat(stackValues.neg);
me.min = Math.min(me.min, helpers.min(values));
me.max = Math.max(me.max, helpers.max(values));
});
me.min = helpers.isFinite(me.min) && !isNaN(me.min) ? me.min : DEFAULT_MIN;
me.max = helpers.isFinite(me.max) && !isNaN(me.max) ? me.max : DEFAULT_MAX;
// Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero
me.handleTickRangeOptions();
},
// Returns the maximum number of ticks based on the scale dimension
_computeTickLimit: function() {
var me = this;
var tickFont;
if (me.isHorizontal()) {
return Math.ceil(me.width / 40);
}
tickFont = helpers.options._parseFont(me.options.ticks);
return Math.ceil(me.height / tickFont.lineHeight);
},
// Called after the ticks are built. We need
handleDirectionalChanges: function() {
if (!this.isHorizontal()) {
// We are in a vertical orientation. The top value is the highest. So reverse the array
this.ticks.reverse();
}
},
getLabelForIndex: function(index, datasetIndex) {
return this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]);
},
// Utils
getPixelForValue: function(value) {
var me = this;
return me.getPixelForDecimal((+me.getRightValue(value) - me._startValue) / me._valueRange);
},
getValueForPixel: function(pixel) {
return this._startValue + this.getDecimalForPixel(pixel) * this._valueRange;
},
getPixelForTick: function(index) {
var ticks = this.ticksAsNumbers;
if (index < 0 || index > ticks.length - 1) {
return null;
}
return this.getPixelForValue(ticks[index]);
}
});
// INTERNAL: static default options, registered in src/index.js
module.exports._defaults = defaultConfig;

View File

@@ -0,0 +1,254 @@
'use strict';
var helpers = require('../helpers/index');
var Scale = require('../core/core.scale');
var noop = helpers.noop;
var isNullOrUndef = helpers.isNullOrUndef;
/**
* Generate a set of linear ticks
* @param generationOptions the options used to generate the ticks
* @param dataRange the range of the data
* @returns {number[]} array of tick values
*/
function generateTicks(generationOptions, dataRange) {
var ticks = [];
// To get a "nice" value for the tick spacing, we will use the appropriately named
// "nice number" algorithm. See https://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks
// for details.
var MIN_SPACING = 1e-14;
var stepSize = generationOptions.stepSize;
var unit = stepSize || 1;
var maxNumSpaces = generationOptions.maxTicks - 1;
var min = generationOptions.min;
var max = generationOptions.max;
var precision = generationOptions.precision;
var rmin = dataRange.min;
var rmax = dataRange.max;
var spacing = helpers.niceNum((rmax - rmin) / maxNumSpaces / unit) * unit;
var factor, niceMin, niceMax, numSpaces;
// Beyond MIN_SPACING floating point numbers being to lose precision
// such that we can't do the math necessary to generate ticks
if (spacing < MIN_SPACING && isNullOrUndef(min) && isNullOrUndef(max)) {
return [rmin, rmax];
}
numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing);
if (numSpaces > maxNumSpaces) {
// If the calculated num of spaces exceeds maxNumSpaces, recalculate it
spacing = helpers.niceNum(numSpaces * spacing / maxNumSpaces / unit) * unit;
}
if (stepSize || isNullOrUndef(precision)) {
// If a precision is not specified, calculate factor based on spacing
factor = Math.pow(10, helpers._decimalPlaces(spacing));
} else {
// If the user specified a precision, round to that number of decimal places
factor = Math.pow(10, precision);
spacing = Math.ceil(spacing * factor) / factor;
}
niceMin = Math.floor(rmin / spacing) * spacing;
niceMax = Math.ceil(rmax / spacing) * spacing;
// If min, max and stepSize is set and they make an evenly spaced scale use it.
if (stepSize) {
// If very close to our whole number, use it.
if (!isNullOrUndef(min) && helpers.almostWhole(min / spacing, spacing / 1000)) {
niceMin = min;
}
if (!isNullOrUndef(max) && helpers.almostWhole(max / spacing, spacing / 1000)) {
niceMax = max;
}
}
numSpaces = (niceMax - niceMin) / spacing;
// If very close to our rounded value, use it.
if (helpers.almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) {
numSpaces = Math.round(numSpaces);
} else {
numSpaces = Math.ceil(numSpaces);
}
niceMin = Math.round(niceMin * factor) / factor;
niceMax = Math.round(niceMax * factor) / factor;
ticks.push(isNullOrUndef(min) ? niceMin : min);
for (var j = 1; j < numSpaces; ++j) {
ticks.push(Math.round((niceMin + j * spacing) * factor) / factor);
}
ticks.push(isNullOrUndef(max) ? niceMax : max);
return ticks;
}
module.exports = Scale.extend({
getRightValue: function(value) {
if (typeof value === 'string') {
return +value;
}
return Scale.prototype.getRightValue.call(this, value);
},
handleTickRangeOptions: function() {
var me = this;
var opts = me.options;
var tickOpts = opts.ticks;
// If we are forcing it to begin at 0, but 0 will already be rendered on the chart,
// do nothing since that would make the chart weird. If the user really wants a weird chart
// axis, they can manually override it
if (tickOpts.beginAtZero) {
var minSign = helpers.sign(me.min);
var maxSign = helpers.sign(me.max);
if (minSign < 0 && maxSign < 0) {
// move the top up to 0
me.max = 0;
} else if (minSign > 0 && maxSign > 0) {
// move the bottom down to 0
me.min = 0;
}
}
var setMin = tickOpts.min !== undefined || tickOpts.suggestedMin !== undefined;
var setMax = tickOpts.max !== undefined || tickOpts.suggestedMax !== undefined;
if (tickOpts.min !== undefined) {
me.min = tickOpts.min;
} else if (tickOpts.suggestedMin !== undefined) {
if (me.min === null) {
me.min = tickOpts.suggestedMin;
} else {
me.min = Math.min(me.min, tickOpts.suggestedMin);
}
}
if (tickOpts.max !== undefined) {
me.max = tickOpts.max;
} else if (tickOpts.suggestedMax !== undefined) {
if (me.max === null) {
me.max = tickOpts.suggestedMax;
} else {
me.max = Math.max(me.max, tickOpts.suggestedMax);
}
}
if (setMin !== setMax) {
// We set the min or the max but not both.
// So ensure that our range is good
// Inverted or 0 length range can happen when
// ticks.min is set, and no datasets are visible
if (me.min >= me.max) {
if (setMin) {
me.max = me.min + 1;
} else {
me.min = me.max - 1;
}
}
}
if (me.min === me.max) {
me.max++;
if (!tickOpts.beginAtZero) {
me.min--;
}
}
},
getTickLimit: function() {
var me = this;
var tickOpts = me.options.ticks;
var stepSize = tickOpts.stepSize;
var maxTicksLimit = tickOpts.maxTicksLimit;
var maxTicks;
if (stepSize) {
maxTicks = Math.ceil(me.max / stepSize) - Math.floor(me.min / stepSize) + 1;
} else {
maxTicks = me._computeTickLimit();
maxTicksLimit = maxTicksLimit || 11;
}
if (maxTicksLimit) {
maxTicks = Math.min(maxTicksLimit, maxTicks);
}
return maxTicks;
},
_computeTickLimit: function() {
return Number.POSITIVE_INFINITY;
},
handleDirectionalChanges: noop,
buildTicks: function() {
var me = this;
var opts = me.options;
var tickOpts = opts.ticks;
// Figure out what the max number of ticks we can support it is based on the size of
// the axis area. For now, we say that the minimum tick spacing in pixels must be 40
// We also limit the maximum number of ticks to 11 which gives a nice 10 squares on
// the graph. Make sure we always have at least 2 ticks
var maxTicks = me.getTickLimit();
maxTicks = Math.max(2, maxTicks);
var numericGeneratorOptions = {
maxTicks: maxTicks,
min: tickOpts.min,
max: tickOpts.max,
precision: tickOpts.precision,
stepSize: helpers.valueOrDefault(tickOpts.fixedStepSize, tickOpts.stepSize)
};
var ticks = me.ticks = generateTicks(numericGeneratorOptions, me);
me.handleDirectionalChanges();
// At this point, we need to update our max and min given the tick values since we have expanded the
// range of the scale
me.max = helpers.max(ticks);
me.min = helpers.min(ticks);
if (tickOpts.reverse) {
ticks.reverse();
me.start = me.max;
me.end = me.min;
} else {
me.start = me.min;
me.end = me.max;
}
},
convertTicksToLabels: function() {
var me = this;
me.ticksAsNumbers = me.ticks.slice();
me.zeroLineIndex = me.ticks.indexOf(0);
Scale.prototype.convertTicksToLabels.call(me);
},
_configure: function() {
var me = this;
var ticks = me.getTicks();
var start = me.min;
var end = me.max;
var offset;
Scale.prototype._configure.call(me);
if (me.options.offset && ticks.length) {
offset = (end - start) / Math.max(ticks.length - 1, 1) / 2;
start -= offset;
end += offset;
}
me._startValue = start;
me._endValue = end;
me._valueRange = end - start;
}
});

View File

@@ -0,0 +1,308 @@
'use strict';
var defaults = require('../core/core.defaults');
var helpers = require('../helpers/index');
var Scale = require('../core/core.scale');
var Ticks = require('../core/core.ticks');
var valueOrDefault = helpers.valueOrDefault;
var log10 = helpers.math.log10;
/**
* Generate a set of logarithmic ticks
* @param generationOptions the options used to generate the ticks
* @param dataRange the range of the data
* @returns {number[]} array of tick values
*/
function generateTicks(generationOptions, dataRange) {
var ticks = [];
var tickVal = valueOrDefault(generationOptions.min, Math.pow(10, Math.floor(log10(dataRange.min))));
var endExp = Math.floor(log10(dataRange.max));
var endSignificand = Math.ceil(dataRange.max / Math.pow(10, endExp));
var exp, significand;
if (tickVal === 0) {
exp = Math.floor(log10(dataRange.minNotZero));
significand = Math.floor(dataRange.minNotZero / Math.pow(10, exp));
ticks.push(tickVal);
tickVal = significand * Math.pow(10, exp);
} else {
exp = Math.floor(log10(tickVal));
significand = Math.floor(tickVal / Math.pow(10, exp));
}
var precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1;
do {
ticks.push(tickVal);
++significand;
if (significand === 10) {
significand = 1;
++exp;
precision = exp >= 0 ? 1 : precision;
}
tickVal = Math.round(significand * Math.pow(10, exp) * precision) / precision;
} while (exp < endExp || (exp === endExp && significand < endSignificand));
var lastTick = valueOrDefault(generationOptions.max, tickVal);
ticks.push(lastTick);
return ticks;
}
var defaultConfig = {
position: 'left',
// label settings
ticks: {
callback: Ticks.formatters.logarithmic
}
};
// TODO(v3): change this to positiveOrDefault
function nonNegativeOrDefault(value, defaultValue) {
return helpers.isFinite(value) && value >= 0 ? value : defaultValue;
}
module.exports = Scale.extend({
determineDataLimits: function() {
var me = this;
var opts = me.options;
var chart = me.chart;
var datasets = chart.data.datasets;
var isHorizontal = me.isHorizontal();
function IDMatches(meta) {
return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id;
}
var datasetIndex, meta, value, data, i, ilen;
// Calculate Range
me.min = Number.POSITIVE_INFINITY;
me.max = Number.NEGATIVE_INFINITY;
me.minNotZero = Number.POSITIVE_INFINITY;
var hasStacks = opts.stacked;
if (hasStacks === undefined) {
for (datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) {
meta = chart.getDatasetMeta(datasetIndex);
if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta) &&
meta.stack !== undefined) {
hasStacks = true;
break;
}
}
}
if (opts.stacked || hasStacks) {
var valuesPerStack = {};
for (datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) {
meta = chart.getDatasetMeta(datasetIndex);
var key = [
meta.type,
// we have a separate stack for stack=undefined datasets when the opts.stacked is undefined
((opts.stacked === undefined && meta.stack === undefined) ? datasetIndex : ''),
meta.stack
].join('.');
if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) {
if (valuesPerStack[key] === undefined) {
valuesPerStack[key] = [];
}
data = datasets[datasetIndex].data;
for (i = 0, ilen = data.length; i < ilen; i++) {
var values = valuesPerStack[key];
value = me._parseValue(data[i]);
// invalid, hidden and negative values are ignored
if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden || value.min < 0 || value.max < 0) {
continue;
}
values[i] = values[i] || 0;
values[i] += value.max;
}
}
}
helpers.each(valuesPerStack, function(valuesForType) {
if (valuesForType.length > 0) {
var minVal = helpers.min(valuesForType);
var maxVal = helpers.max(valuesForType);
me.min = Math.min(me.min, minVal);
me.max = Math.max(me.max, maxVal);
}
});
} else {
for (datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) {
meta = chart.getDatasetMeta(datasetIndex);
if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) {
data = datasets[datasetIndex].data;
for (i = 0, ilen = data.length; i < ilen; i++) {
value = me._parseValue(data[i]);
// invalid, hidden and negative values are ignored
if (isNaN(value.min) || isNaN(value.max) || meta.data[i].hidden || value.min < 0 || value.max < 0) {
continue;
}
me.min = Math.min(value.min, me.min);
me.max = Math.max(value.max, me.max);
if (value.min !== 0) {
me.minNotZero = Math.min(value.min, me.minNotZero);
}
}
}
}
}
me.min = helpers.isFinite(me.min) ? me.min : null;
me.max = helpers.isFinite(me.max) ? me.max : null;
me.minNotZero = helpers.isFinite(me.minNotZero) ? me.minNotZero : null;
// Common base implementation to handle ticks.min, ticks.max
this.handleTickRangeOptions();
},
handleTickRangeOptions: function() {
var me = this;
var tickOpts = me.options.ticks;
var DEFAULT_MIN = 1;
var DEFAULT_MAX = 10;
me.min = nonNegativeOrDefault(tickOpts.min, me.min);
me.max = nonNegativeOrDefault(tickOpts.max, me.max);
if (me.min === me.max) {
if (me.min !== 0 && me.min !== null) {
me.min = Math.pow(10, Math.floor(log10(me.min)) - 1);
me.max = Math.pow(10, Math.floor(log10(me.max)) + 1);
} else {
me.min = DEFAULT_MIN;
me.max = DEFAULT_MAX;
}
}
if (me.min === null) {
me.min = Math.pow(10, Math.floor(log10(me.max)) - 1);
}
if (me.max === null) {
me.max = me.min !== 0
? Math.pow(10, Math.floor(log10(me.min)) + 1)
: DEFAULT_MAX;
}
if (me.minNotZero === null) {
if (me.min > 0) {
me.minNotZero = me.min;
} else if (me.max < 1) {
me.minNotZero = Math.pow(10, Math.floor(log10(me.max)));
} else {
me.minNotZero = DEFAULT_MIN;
}
}
},
buildTicks: function() {
var me = this;
var tickOpts = me.options.ticks;
var reverse = !me.isHorizontal();
var generationOptions = {
min: nonNegativeOrDefault(tickOpts.min),
max: nonNegativeOrDefault(tickOpts.max)
};
var ticks = me.ticks = generateTicks(generationOptions, me);
// At this point, we need to update our max and min given the tick values since we have expanded the
// range of the scale
me.max = helpers.max(ticks);
me.min = helpers.min(ticks);
if (tickOpts.reverse) {
reverse = !reverse;
me.start = me.max;
me.end = me.min;
} else {
me.start = me.min;
me.end = me.max;
}
if (reverse) {
ticks.reverse();
}
},
convertTicksToLabels: function() {
this.tickValues = this.ticks.slice();
Scale.prototype.convertTicksToLabels.call(this);
},
// Get the correct tooltip label
getLabelForIndex: function(index, datasetIndex) {
return this._getScaleLabel(this.chart.data.datasets[datasetIndex].data[index]);
},
getPixelForTick: function(index) {
var ticks = this.tickValues;
if (index < 0 || index > ticks.length - 1) {
return null;
}
return this.getPixelForValue(ticks[index]);
},
/**
* Returns the value of the first tick.
* @param {number} value - The minimum not zero value.
* @return {number} The first tick value.
* @private
*/
_getFirstTickValue: function(value) {
var exp = Math.floor(log10(value));
var significand = Math.floor(value / Math.pow(10, exp));
return significand * Math.pow(10, exp);
},
_configure: function() {
var me = this;
var start = me.min;
var offset = 0;
Scale.prototype._configure.call(me);
if (start === 0) {
start = me._getFirstTickValue(me.minNotZero);
offset = valueOrDefault(me.options.ticks.fontSize, defaults.global.defaultFontSize) / me._length;
}
me._startValue = log10(start);
me._valueOffset = offset;
me._valueRange = (log10(me.max) - log10(start)) / (1 - offset);
},
getPixelForValue: function(value) {
var me = this;
var decimal = 0;
value = +me.getRightValue(value);
if (value > me.min && value > 0) {
decimal = (log10(value) - me._startValue) / me._valueRange + me._valueOffset;
}
return me.getPixelForDecimal(decimal);
},
getValueForPixel: function(pixel) {
var me = this;
var decimal = me.getDecimalForPixel(pixel);
return decimal === 0 && me.min === 0
? 0
: Math.pow(10, me._startValue + (decimal - me._valueOffset) * me._valueRange);
}
});
// INTERNAL: static default options, registered in src/index.js
module.exports._defaults = defaultConfig;

View File

@@ -0,0 +1,558 @@
'use strict';
var defaults = require('../core/core.defaults');
var helpers = require('../helpers/index');
var LinearScaleBase = require('./scale.linearbase');
var Ticks = require('../core/core.ticks');
var valueOrDefault = helpers.valueOrDefault;
var valueAtIndexOrDefault = helpers.valueAtIndexOrDefault;
var resolve = helpers.options.resolve;
var defaultConfig = {
display: true,
// Boolean - Whether to animate scaling the chart from the centre
animate: true,
position: 'chartArea',
angleLines: {
display: true,
color: 'rgba(0,0,0,0.1)',
lineWidth: 1,
borderDash: [],
borderDashOffset: 0.0
},
gridLines: {
circular: false
},
// label settings
ticks: {
// Boolean - Show a backdrop to the scale label
showLabelBackdrop: true,
// String - The colour of the label backdrop
backdropColor: 'rgba(255,255,255,0.75)',
// Number - The backdrop padding above & below the label in pixels
backdropPaddingY: 2,
// Number - The backdrop padding to the side of the label in pixels
backdropPaddingX: 2,
callback: Ticks.formatters.linear
},
pointLabels: {
// Boolean - if true, show point labels
display: true,
// Number - Point label font size in pixels
fontSize: 10,
// Function - Used to convert point labels
callback: function(label) {
return label;
}
}
};
function getTickBackdropHeight(opts) {
var tickOpts = opts.ticks;
if (tickOpts.display && opts.display) {
return valueOrDefault(tickOpts.fontSize, defaults.global.defaultFontSize) + tickOpts.backdropPaddingY * 2;
}
return 0;
}
function measureLabelSize(ctx, lineHeight, label) {
if (helpers.isArray(label)) {
return {
w: helpers.longestText(ctx, ctx.font, label),
h: label.length * lineHeight
};
}
return {
w: ctx.measureText(label).width,
h: lineHeight
};
}
function determineLimits(angle, pos, size, min, max) {
if (angle === min || angle === max) {
return {
start: pos - (size / 2),
end: pos + (size / 2)
};
} else if (angle < min || angle > max) {
return {
start: pos - size,
end: pos
};
}
return {
start: pos,
end: pos + size
};
}
/**
* Helper function to fit a radial linear scale with point labels
*/
function fitWithPointLabels(scale) {
// Right, this is really confusing and there is a lot of maths going on here
// The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
//
// Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
//
// Solution:
//
// We assume the radius of the polygon is half the size of the canvas at first
// at each index we check if the text overlaps.
//
// Where it does, we store that angle and that index.
//
// After finding the largest index and angle we calculate how much we need to remove
// from the shape radius to move the point inwards by that x.
//
// We average the left and right distances to get the maximum shape radius that can fit in the box
// along with labels.
//
// Once we have that, we can find the centre point for the chart, by taking the x text protrusion
// on each side, removing that from the size, halving it and adding the left x protrusion width.
//
// This will mean we have a shape fitted to the canvas, as large as it can be with the labels
// and position it in the most space efficient manner
//
// https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
var plFont = helpers.options._parseFont(scale.options.pointLabels);
// Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
// Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
var furthestLimits = {
l: 0,
r: scale.width,
t: 0,
b: scale.height - scale.paddingTop
};
var furthestAngles = {};
var i, textSize, pointPosition;
scale.ctx.font = plFont.string;
scale._pointLabelSizes = [];
var valueCount = scale.chart.data.labels.length;
for (i = 0; i < valueCount; i++) {
pointPosition = scale.getPointPosition(i, scale.drawingArea + 5);
textSize = measureLabelSize(scale.ctx, plFont.lineHeight, scale.pointLabels[i]);
scale._pointLabelSizes[i] = textSize;
// Add quarter circle to make degree 0 mean top of circle
var angleRadians = scale.getIndexAngle(i);
var angle = helpers.toDegrees(angleRadians) % 360;
var hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180);
var vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270);
if (hLimits.start < furthestLimits.l) {
furthestLimits.l = hLimits.start;
furthestAngles.l = angleRadians;
}
if (hLimits.end > furthestLimits.r) {
furthestLimits.r = hLimits.end;
furthestAngles.r = angleRadians;
}
if (vLimits.start < furthestLimits.t) {
furthestLimits.t = vLimits.start;
furthestAngles.t = angleRadians;
}
if (vLimits.end > furthestLimits.b) {
furthestLimits.b = vLimits.end;
furthestAngles.b = angleRadians;
}
}
scale.setReductions(scale.drawingArea, furthestLimits, furthestAngles);
}
function getTextAlignForAngle(angle) {
if (angle === 0 || angle === 180) {
return 'center';
} else if (angle < 180) {
return 'left';
}
return 'right';
}
function fillText(ctx, text, position, lineHeight) {
var y = position.y + lineHeight / 2;
var i, ilen;
if (helpers.isArray(text)) {
for (i = 0, ilen = text.length; i < ilen; ++i) {
ctx.fillText(text[i], position.x, y);
y += lineHeight;
}
} else {
ctx.fillText(text, position.x, y);
}
}
function adjustPointPositionForLabelHeight(angle, textSize, position) {
if (angle === 90 || angle === 270) {
position.y -= (textSize.h / 2);
} else if (angle > 270 || angle < 90) {
position.y -= textSize.h;
}
}
function drawPointLabels(scale) {
var ctx = scale.ctx;
var opts = scale.options;
var pointLabelOpts = opts.pointLabels;
var tickBackdropHeight = getTickBackdropHeight(opts);
var outerDistance = scale.getDistanceFromCenterForValue(opts.ticks.reverse ? scale.min : scale.max);
var plFont = helpers.options._parseFont(pointLabelOpts);
ctx.save();
ctx.font = plFont.string;
ctx.textBaseline = 'middle';
for (var i = scale.chart.data.labels.length - 1; i >= 0; i--) {
// Extra pixels out for some label spacing
var extra = (i === 0 ? tickBackdropHeight / 2 : 0);
var pointLabelPosition = scale.getPointPosition(i, outerDistance + extra + 5);
// Keep this in loop since we may support array properties here
var pointLabelFontColor = valueAtIndexOrDefault(pointLabelOpts.fontColor, i, defaults.global.defaultFontColor);
ctx.fillStyle = pointLabelFontColor;
var angleRadians = scale.getIndexAngle(i);
var angle = helpers.toDegrees(angleRadians);
ctx.textAlign = getTextAlignForAngle(angle);
adjustPointPositionForLabelHeight(angle, scale._pointLabelSizes[i], pointLabelPosition);
fillText(ctx, scale.pointLabels[i], pointLabelPosition, plFont.lineHeight);
}
ctx.restore();
}
function drawRadiusLine(scale, gridLineOpts, radius, index) {
var ctx = scale.ctx;
var circular = gridLineOpts.circular;
var valueCount = scale.chart.data.labels.length;
var lineColor = valueAtIndexOrDefault(gridLineOpts.color, index - 1);
var lineWidth = valueAtIndexOrDefault(gridLineOpts.lineWidth, index - 1);
var pointPosition;
if ((!circular && !valueCount) || !lineColor || !lineWidth) {
return;
}
ctx.save();
ctx.strokeStyle = lineColor;
ctx.lineWidth = lineWidth;
if (ctx.setLineDash) {
ctx.setLineDash(gridLineOpts.borderDash || []);
ctx.lineDashOffset = gridLineOpts.borderDashOffset || 0.0;
}
ctx.beginPath();
if (circular) {
// Draw circular arcs between the points
ctx.arc(scale.xCenter, scale.yCenter, radius, 0, Math.PI * 2);
} else {
// Draw straight lines connecting each index
pointPosition = scale.getPointPosition(0, radius);
ctx.moveTo(pointPosition.x, pointPosition.y);
for (var i = 1; i < valueCount; i++) {
pointPosition = scale.getPointPosition(i, radius);
ctx.lineTo(pointPosition.x, pointPosition.y);
}
}
ctx.closePath();
ctx.stroke();
ctx.restore();
}
function numberOrZero(param) {
return helpers.isNumber(param) ? param : 0;
}
module.exports = LinearScaleBase.extend({
setDimensions: function() {
var me = this;
// Set the unconstrained dimension before label rotation
me.width = me.maxWidth;
me.height = me.maxHeight;
me.paddingTop = getTickBackdropHeight(me.options) / 2;
me.xCenter = Math.floor(me.width / 2);
me.yCenter = Math.floor((me.height - me.paddingTop) / 2);
me.drawingArea = Math.min(me.height - me.paddingTop, me.width) / 2;
},
determineDataLimits: function() {
var me = this;
var chart = me.chart;
var min = Number.POSITIVE_INFINITY;
var max = Number.NEGATIVE_INFINITY;
helpers.each(chart.data.datasets, function(dataset, datasetIndex) {
if (chart.isDatasetVisible(datasetIndex)) {
var meta = chart.getDatasetMeta(datasetIndex);
helpers.each(dataset.data, function(rawValue, index) {
var value = +me.getRightValue(rawValue);
if (isNaN(value) || meta.data[index].hidden) {
return;
}
min = Math.min(value, min);
max = Math.max(value, max);
});
}
});
me.min = (min === Number.POSITIVE_INFINITY ? 0 : min);
me.max = (max === Number.NEGATIVE_INFINITY ? 0 : max);
// Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero
me.handleTickRangeOptions();
},
// Returns the maximum number of ticks based on the scale dimension
_computeTickLimit: function() {
return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options));
},
convertTicksToLabels: function() {
var me = this;
LinearScaleBase.prototype.convertTicksToLabels.call(me);
// Point labels
me.pointLabels = me.chart.data.labels.map(function() {
var label = helpers.callback(me.options.pointLabels.callback, arguments, me);
return label || label === 0 ? label : '';
});
},
getLabelForIndex: function(index, datasetIndex) {
return +this.getRightValue(this.chart.data.datasets[datasetIndex].data[index]);
},
fit: function() {
var me = this;
var opts = me.options;
if (opts.display && opts.pointLabels.display) {
fitWithPointLabels(me);
} else {
me.setCenterPoint(0, 0, 0, 0);
}
},
/**
* Set radius reductions and determine new radius and center point
* @private
*/
setReductions: function(largestPossibleRadius, furthestLimits, furthestAngles) {
var me = this;
var radiusReductionLeft = furthestLimits.l / Math.sin(furthestAngles.l);
var radiusReductionRight = Math.max(furthestLimits.r - me.width, 0) / Math.sin(furthestAngles.r);
var radiusReductionTop = -furthestLimits.t / Math.cos(furthestAngles.t);
var radiusReductionBottom = -Math.max(furthestLimits.b - (me.height - me.paddingTop), 0) / Math.cos(furthestAngles.b);
radiusReductionLeft = numberOrZero(radiusReductionLeft);
radiusReductionRight = numberOrZero(radiusReductionRight);
radiusReductionTop = numberOrZero(radiusReductionTop);
radiusReductionBottom = numberOrZero(radiusReductionBottom);
me.drawingArea = Math.min(
Math.floor(largestPossibleRadius - (radiusReductionLeft + radiusReductionRight) / 2),
Math.floor(largestPossibleRadius - (radiusReductionTop + radiusReductionBottom) / 2));
me.setCenterPoint(radiusReductionLeft, radiusReductionRight, radiusReductionTop, radiusReductionBottom);
},
setCenterPoint: function(leftMovement, rightMovement, topMovement, bottomMovement) {
var me = this;
var maxRight = me.width - rightMovement - me.drawingArea;
var maxLeft = leftMovement + me.drawingArea;
var maxTop = topMovement + me.drawingArea;
var maxBottom = (me.height - me.paddingTop) - bottomMovement - me.drawingArea;
me.xCenter = Math.floor(((maxLeft + maxRight) / 2) + me.left);
me.yCenter = Math.floor(((maxTop + maxBottom) / 2) + me.top + me.paddingTop);
},
getIndexAngle: function(index) {
var chart = this.chart;
var angleMultiplier = 360 / chart.data.labels.length;
var options = chart.options || {};
var startAngle = options.startAngle || 0;
// Start from the top instead of right, so remove a quarter of the circle
var angle = (index * angleMultiplier + startAngle) % 360;
return (angle < 0 ? angle + 360 : angle) * Math.PI * 2 / 360;
},
getDistanceFromCenterForValue: function(value) {
var me = this;
if (helpers.isNullOrUndef(value)) {
return NaN;
}
// Take into account half font size + the yPadding of the top value
var scalingFactor = me.drawingArea / (me.max - me.min);
if (me.options.ticks.reverse) {
return (me.max - value) * scalingFactor;
}
return (value - me.min) * scalingFactor;
},
getPointPosition: function(index, distanceFromCenter) {
var me = this;
var thisAngle = me.getIndexAngle(index) - (Math.PI / 2);
return {
x: Math.cos(thisAngle) * distanceFromCenter + me.xCenter,
y: Math.sin(thisAngle) * distanceFromCenter + me.yCenter
};
},
getPointPositionForValue: function(index, value) {
return this.getPointPosition(index, this.getDistanceFromCenterForValue(value));
},
getBasePosition: function(index) {
var me = this;
var min = me.min;
var max = me.max;
return me.getPointPositionForValue(index || 0,
me.beginAtZero ? 0 :
min < 0 && max < 0 ? max :
min > 0 && max > 0 ? min :
0);
},
/**
* @private
*/
_drawGrid: function() {
var me = this;
var ctx = me.ctx;
var opts = me.options;
var gridLineOpts = opts.gridLines;
var angleLineOpts = opts.angleLines;
var lineWidth = valueOrDefault(angleLineOpts.lineWidth, gridLineOpts.lineWidth);
var lineColor = valueOrDefault(angleLineOpts.color, gridLineOpts.color);
var i, offset, position;
if (opts.pointLabels.display) {
drawPointLabels(me);
}
if (gridLineOpts.display) {
helpers.each(me.ticks, function(label, index) {
if (index !== 0) {
offset = me.getDistanceFromCenterForValue(me.ticksAsNumbers[index]);
drawRadiusLine(me, gridLineOpts, offset, index);
}
});
}
if (angleLineOpts.display && lineWidth && lineColor) {
ctx.save();
ctx.lineWidth = lineWidth;
ctx.strokeStyle = lineColor;
if (ctx.setLineDash) {
ctx.setLineDash(resolve([angleLineOpts.borderDash, gridLineOpts.borderDash, []]));
ctx.lineDashOffset = resolve([angleLineOpts.borderDashOffset, gridLineOpts.borderDashOffset, 0.0]);
}
for (i = me.chart.data.labels.length - 1; i >= 0; i--) {
offset = me.getDistanceFromCenterForValue(opts.ticks.reverse ? me.min : me.max);
position = me.getPointPosition(i, offset);
ctx.beginPath();
ctx.moveTo(me.xCenter, me.yCenter);
ctx.lineTo(position.x, position.y);
ctx.stroke();
}
ctx.restore();
}
},
/**
* @private
*/
_drawLabels: function() {
var me = this;
var ctx = me.ctx;
var opts = me.options;
var tickOpts = opts.ticks;
if (!tickOpts.display) {
return;
}
var startAngle = me.getIndexAngle(0);
var tickFont = helpers.options._parseFont(tickOpts);
var tickFontColor = valueOrDefault(tickOpts.fontColor, defaults.global.defaultFontColor);
var offset, width;
ctx.save();
ctx.font = tickFont.string;
ctx.translate(me.xCenter, me.yCenter);
ctx.rotate(startAngle);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
helpers.each(me.ticks, function(label, index) {
if (index === 0 && !tickOpts.reverse) {
return;
}
offset = me.getDistanceFromCenterForValue(me.ticksAsNumbers[index]);
if (tickOpts.showLabelBackdrop) {
width = ctx.measureText(label).width;
ctx.fillStyle = tickOpts.backdropColor;
ctx.fillRect(
-width / 2 - tickOpts.backdropPaddingX,
-offset - tickFont.size / 2 - tickOpts.backdropPaddingY,
width + tickOpts.backdropPaddingX * 2,
tickFont.size + tickOpts.backdropPaddingY * 2
);
}
ctx.fillStyle = tickFontColor;
ctx.fillText(label, 0, -offset);
});
ctx.restore();
},
/**
* @private
*/
_drawTitle: helpers.noop
});
// INTERNAL: static default options, registered in src/index.js
module.exports._defaults = defaultConfig;

View File

@@ -0,0 +1,764 @@
'use strict';
var adapters = require('../core/core.adapters');
var defaults = require('../core/core.defaults');
var helpers = require('../helpers/index');
var Scale = require('../core/core.scale');
var deprecated = helpers._deprecated;
var resolve = helpers.options.resolve;
var valueOrDefault = helpers.valueOrDefault;
// Integer constants are from the ES6 spec.
var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991;
var MAX_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991;
var INTERVALS = {
millisecond: {
common: true,
size: 1,
steps: 1000
},
second: {
common: true,
size: 1000,
steps: 60
},
minute: {
common: true,
size: 60000,
steps: 60
},
hour: {
common: true,
size: 3600000,
steps: 24
},
day: {
common: true,
size: 86400000,
steps: 30
},
week: {
common: false,
size: 604800000,
steps: 4
},
month: {
common: true,
size: 2.628e9,
steps: 12
},
quarter: {
common: false,
size: 7.884e9,
steps: 4
},
year: {
common: true,
size: 3.154e10
}
};
var UNITS = Object.keys(INTERVALS);
function sorter(a, b) {
return a - b;
}
function arrayUnique(items) {
var hash = {};
var out = [];
var i, ilen, item;
for (i = 0, ilen = items.length; i < ilen; ++i) {
item = items[i];
if (!hash[item]) {
hash[item] = true;
out.push(item);
}
}
return out;
}
function getMin(options) {
return helpers.valueOrDefault(options.time.min, options.ticks.min);
}
function getMax(options) {
return helpers.valueOrDefault(options.time.max, options.ticks.max);
}
/**
* Returns an array of {time, pos} objects used to interpolate a specific `time` or position
* (`pos`) on the scale, by searching entries before and after the requested value. `pos` is
* a decimal between 0 and 1: 0 being the start of the scale (left or top) and 1 the other
* extremity (left + width or top + height). Note that it would be more optimized to directly
* store pre-computed pixels, but the scale dimensions are not guaranteed at the time we need
* to create the lookup table. The table ALWAYS contains at least two items: min and max.
*
* @param {number[]} timestamps - timestamps sorted from lowest to highest.
* @param {string} distribution - If 'linear', timestamps will be spread linearly along the min
* and max range, so basically, the table will contains only two items: {min, 0} and {max, 1}.
* If 'series', timestamps will be positioned at the same distance from each other. In this
* case, only timestamps that break the time linearity are registered, meaning that in the
* best case, all timestamps are linear, the table contains only min and max.
*/
function buildLookupTable(timestamps, min, max, distribution) {
if (distribution === 'linear' || !timestamps.length) {
return [
{time: min, pos: 0},
{time: max, pos: 1}
];
}
var table = [];
var items = [min];
var i, ilen, prev, curr, next;
for (i = 0, ilen = timestamps.length; i < ilen; ++i) {
curr = timestamps[i];
if (curr > min && curr < max) {
items.push(curr);
}
}
items.push(max);
for (i = 0, ilen = items.length; i < ilen; ++i) {
next = items[i + 1];
prev = items[i - 1];
curr = items[i];
// only add points that breaks the scale linearity
if (prev === undefined || next === undefined || Math.round((next + prev) / 2) !== curr) {
table.push({time: curr, pos: i / (ilen - 1)});
}
}
return table;
}
// @see adapted from https://www.anujgakhar.com/2014/03/01/binary-search-in-javascript/
function lookup(table, key, value) {
var lo = 0;
var hi = table.length - 1;
var mid, i0, i1;
while (lo >= 0 && lo <= hi) {
mid = (lo + hi) >> 1;
i0 = table[mid - 1] || null;
i1 = table[mid];
if (!i0) {
// given value is outside table (before first item)
return {lo: null, hi: i1};
} else if (i1[key] < value) {
lo = mid + 1;
} else if (i0[key] > value) {
hi = mid - 1;
} else {
return {lo: i0, hi: i1};
}
}
// given value is outside table (after last item)
return {lo: i1, hi: null};
}
/**
* Linearly interpolates the given source `value` using the table items `skey` values and
* returns the associated `tkey` value. For example, interpolate(table, 'time', 42, 'pos')
* returns the position for a timestamp equal to 42. If value is out of bounds, values at
* index [0, 1] or [n - 1, n] are used for the interpolation.
*/
function interpolate(table, skey, sval, tkey) {
var range = lookup(table, skey, sval);
// Note: the lookup table ALWAYS contains at least 2 items (min and max)
var prev = !range.lo ? table[0] : !range.hi ? table[table.length - 2] : range.lo;
var next = !range.lo ? table[1] : !range.hi ? table[table.length - 1] : range.hi;
var span = next[skey] - prev[skey];
var ratio = span ? (sval - prev[skey]) / span : 0;
var offset = (next[tkey] - prev[tkey]) * ratio;
return prev[tkey] + offset;
}
function toTimestamp(scale, input) {
var adapter = scale._adapter;
var options = scale.options.time;
var parser = options.parser;
var format = parser || options.format;
var value = input;
if (typeof parser === 'function') {
value = parser(value);
}
// Only parse if its not a timestamp already
if (!helpers.isFinite(value)) {
value = typeof format === 'string'
? adapter.parse(value, format)
: adapter.parse(value);
}
if (value !== null) {
return +value;
}
// Labels are in an incompatible format and no `parser` has been provided.
// The user might still use the deprecated `format` option for parsing.
if (!parser && typeof format === 'function') {
value = format(input);
// `format` could return something else than a timestamp, if so, parse it
if (!helpers.isFinite(value)) {
value = adapter.parse(value);
}
}
return value;
}
function parse(scale, input) {
if (helpers.isNullOrUndef(input)) {
return null;
}
var options = scale.options.time;
var value = toTimestamp(scale, scale.getRightValue(input));
if (value === null) {
return value;
}
if (options.round) {
value = +scale._adapter.startOf(value, options.round);
}
return value;
}
/**
* Figures out what unit results in an appropriate number of auto-generated ticks
*/
function determineUnitForAutoTicks(minUnit, min, max, capacity) {
var ilen = UNITS.length;
var i, interval, factor;
for (i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) {
interval = INTERVALS[UNITS[i]];
factor = interval.steps ? interval.steps : MAX_INTEGER;
if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) {
return UNITS[i];
}
}
return UNITS[ilen - 1];
}
/**
* Figures out what unit to format a set of ticks with
*/
function determineUnitForFormatting(scale, numTicks, minUnit, min, max) {
var i, unit;
for (i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--) {
unit = UNITS[i];
if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= numTicks - 1) {
return unit;
}
}
return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0];
}
function determineMajorUnit(unit) {
for (var i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) {
if (INTERVALS[UNITS[i]].common) {
return UNITS[i];
}
}
}
/**
* Generates a maximum of `capacity` timestamps between min and max, rounded to the
* `minor` unit using the given scale time `options`.
* Important: this method can return ticks outside the min and max range, it's the
* responsibility of the calling code to clamp values if needed.
*/
function generate(scale, min, max, capacity) {
var adapter = scale._adapter;
var options = scale.options;
var timeOpts = options.time;
var minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity);
var stepSize = resolve([timeOpts.stepSize, timeOpts.unitStepSize, 1]);
var weekday = minor === 'week' ? timeOpts.isoWeekday : false;
var first = min;
var ticks = [];
var time;
// For 'week' unit, handle the first day of week option
if (weekday) {
first = +adapter.startOf(first, 'isoWeek', weekday);
}
// Align first ticks on unit
first = +adapter.startOf(first, weekday ? 'day' : minor);
// Prevent browser from freezing in case user options request millions of milliseconds
if (adapter.diff(max, min, minor) > 100000 * stepSize) {
throw min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor;
}
for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) {
ticks.push(time);
}
if (time === max || options.bounds === 'ticks') {
ticks.push(time);
}
return ticks;
}
/**
* Returns the start and end offsets from edges in the form of {start, end}
* where each value is a relative width to the scale and ranges between 0 and 1.
* They add extra margins on the both sides by scaling down the original scale.
* Offsets are added when the `offset` option is true.
*/
function computeOffsets(table, ticks, min, max, options) {
var start = 0;
var end = 0;
var first, last;
if (options.offset && ticks.length) {
first = interpolate(table, 'time', ticks[0], 'pos');
if (ticks.length === 1) {
start = 1 - first;
} else {
start = (interpolate(table, 'time', ticks[1], 'pos') - first) / 2;
}
last = interpolate(table, 'time', ticks[ticks.length - 1], 'pos');
if (ticks.length === 1) {
end = last;
} else {
end = (last - interpolate(table, 'time', ticks[ticks.length - 2], 'pos')) / 2;
}
}
return {start: start, end: end, factor: 1 / (start + 1 + end)};
}
function setMajorTicks(scale, ticks, map, majorUnit) {
var adapter = scale._adapter;
var first = +adapter.startOf(ticks[0].value, majorUnit);
var last = ticks[ticks.length - 1].value;
var major, index;
for (major = first; major <= last; major = +adapter.add(major, 1, majorUnit)) {
index = map[major];
if (index >= 0) {
ticks[index].major = true;
}
}
return ticks;
}
function ticksFromTimestamps(scale, values, majorUnit) {
var ticks = [];
var map = {};
var ilen = values.length;
var i, value;
for (i = 0; i < ilen; ++i) {
value = values[i];
map[value] = i;
ticks.push({
value: value,
major: false
});
}
// We set the major ticks separately from the above loop because calling startOf for every tick
// is expensive when there is a large number of ticks
return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit);
}
var defaultConfig = {
position: 'bottom',
/**
* Data distribution along the scale:
* - 'linear': data are spread according to their time (distances can vary),
* - 'series': data are spread at the same distance from each other.
* @see https://github.com/chartjs/Chart.js/pull/4507
* @since 2.7.0
*/
distribution: 'linear',
/**
* Scale boundary strategy (bypassed by min/max time options)
* - `data`: make sure data are fully visible, ticks outside are removed
* - `ticks`: make sure ticks are fully visible, data outside are truncated
* @see https://github.com/chartjs/Chart.js/pull/4556
* @since 2.7.0
*/
bounds: 'data',
adapters: {},
time: {
parser: false, // false == a pattern string from https://momentjs.com/docs/#/parsing/string-format/ or a custom callback that converts its argument to a moment
unit: false, // false == automatic or override with week, month, year, etc.
round: false, // none, or override with week, month, year, etc.
displayFormat: false, // DEPRECATED
isoWeekday: false, // override week start day - see https://momentjs.com/docs/#/get-set/iso-weekday/
minUnit: 'millisecond',
displayFormats: {}
},
ticks: {
autoSkip: false,
/**
* Ticks generation input values:
* - 'auto': generates "optimal" ticks based on scale size and time options.
* - 'data': generates ticks from data (including labels from data {t|x|y} objects).
* - 'labels': generates ticks from user given `data.labels` values ONLY.
* @see https://github.com/chartjs/Chart.js/pull/4507
* @since 2.7.0
*/
source: 'auto',
major: {
enabled: false
}
}
};
module.exports = Scale.extend({
initialize: function() {
this.mergeTicksOptions();
Scale.prototype.initialize.call(this);
},
update: function() {
var me = this;
var options = me.options;
var time = options.time || (options.time = {});
var adapter = me._adapter = new adapters._date(options.adapters.date);
// DEPRECATIONS: output a message only one time per update
deprecated('time scale', time.format, 'time.format', 'time.parser');
deprecated('time scale', time.min, 'time.min', 'ticks.min');
deprecated('time scale', time.max, 'time.max', 'ticks.max');
// Backward compatibility: before introducing adapter, `displayFormats` was
// supposed to contain *all* unit/string pairs but this can't be resolved
// when loading the scale (adapters are loaded afterward), so let's populate
// missing formats on update
helpers.mergeIf(time.displayFormats, adapter.formats());
return Scale.prototype.update.apply(me, arguments);
},
/**
* Allows data to be referenced via 't' attribute
*/
getRightValue: function(rawValue) {
if (rawValue && rawValue.t !== undefined) {
rawValue = rawValue.t;
}
return Scale.prototype.getRightValue.call(this, rawValue);
},
determineDataLimits: function() {
var me = this;
var chart = me.chart;
var adapter = me._adapter;
var options = me.options;
var unit = options.time.unit || 'day';
var min = MAX_INTEGER;
var max = MIN_INTEGER;
var timestamps = [];
var datasets = [];
var labels = [];
var i, j, ilen, jlen, data, timestamp, labelsAdded;
var dataLabels = me._getLabels();
for (i = 0, ilen = dataLabels.length; i < ilen; ++i) {
labels.push(parse(me, dataLabels[i]));
}
for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) {
if (chart.isDatasetVisible(i)) {
data = chart.data.datasets[i].data;
// Let's consider that all data have the same format.
if (helpers.isObject(data[0])) {
datasets[i] = [];
for (j = 0, jlen = data.length; j < jlen; ++j) {
timestamp = parse(me, data[j]);
timestamps.push(timestamp);
datasets[i][j] = timestamp;
}
} else {
datasets[i] = labels.slice(0);
if (!labelsAdded) {
timestamps = timestamps.concat(labels);
labelsAdded = true;
}
}
} else {
datasets[i] = [];
}
}
if (labels.length) {
min = Math.min(min, labels[0]);
max = Math.max(max, labels[labels.length - 1]);
}
if (timestamps.length) {
timestamps = ilen > 1 ? arrayUnique(timestamps).sort(sorter) : timestamps.sort(sorter);
min = Math.min(min, timestamps[0]);
max = Math.max(max, timestamps[timestamps.length - 1]);
}
min = parse(me, getMin(options)) || min;
max = parse(me, getMax(options)) || max;
// In case there is no valid min/max, set limits based on unit time option
min = min === MAX_INTEGER ? +adapter.startOf(Date.now(), unit) : min;
max = max === MIN_INTEGER ? +adapter.endOf(Date.now(), unit) + 1 : max;
// Make sure that max is strictly higher than min (required by the lookup table)
me.min = Math.min(min, max);
me.max = Math.max(min + 1, max);
// PRIVATE
me._table = [];
me._timestamps = {
data: timestamps,
datasets: datasets,
labels: labels
};
},
buildTicks: function() {
var me = this;
var min = me.min;
var max = me.max;
var options = me.options;
var tickOpts = options.ticks;
var timeOpts = options.time;
var timestamps = me._timestamps;
var ticks = [];
var capacity = me.getLabelCapacity(min);
var source = tickOpts.source;
var distribution = options.distribution;
var i, ilen, timestamp;
if (source === 'data' || (source === 'auto' && distribution === 'series')) {
timestamps = timestamps.data;
} else if (source === 'labels') {
timestamps = timestamps.labels;
} else {
timestamps = generate(me, min, max, capacity, options);
}
if (options.bounds === 'ticks' && timestamps.length) {
min = timestamps[0];
max = timestamps[timestamps.length - 1];
}
// Enforce limits with user min/max options
min = parse(me, getMin(options)) || min;
max = parse(me, getMax(options)) || max;
// Remove ticks outside the min/max range
for (i = 0, ilen = timestamps.length; i < ilen; ++i) {
timestamp = timestamps[i];
if (timestamp >= min && timestamp <= max) {
ticks.push(timestamp);
}
}
me.min = min;
me.max = max;
// PRIVATE
// determineUnitForFormatting relies on the number of ticks so we don't use it when
// autoSkip is enabled because we don't yet know what the final number of ticks will be
me._unit = timeOpts.unit || (tickOpts.autoSkip
? determineUnitForAutoTicks(timeOpts.minUnit, me.min, me.max, capacity)
: determineUnitForFormatting(me, ticks.length, timeOpts.minUnit, me.min, me.max));
me._majorUnit = !tickOpts.major.enabled || me._unit === 'year' ? undefined
: determineMajorUnit(me._unit);
me._table = buildLookupTable(me._timestamps.data, min, max, distribution);
me._offsets = computeOffsets(me._table, ticks, min, max, options);
if (tickOpts.reverse) {
ticks.reverse();
}
return ticksFromTimestamps(me, ticks, me._majorUnit);
},
getLabelForIndex: function(index, datasetIndex) {
var me = this;
var adapter = me._adapter;
var data = me.chart.data;
var timeOpts = me.options.time;
var label = data.labels && index < data.labels.length ? data.labels[index] : '';
var value = data.datasets[datasetIndex].data[index];
if (helpers.isObject(value)) {
label = me.getRightValue(value);
}
if (timeOpts.tooltipFormat) {
return adapter.format(toTimestamp(me, label), timeOpts.tooltipFormat);
}
if (typeof label === 'string') {
return label;
}
return adapter.format(toTimestamp(me, label), timeOpts.displayFormats.datetime);
},
/**
* Function to format an individual tick mark
* @private
*/
tickFormatFunction: function(time, index, ticks, format) {
var me = this;
var adapter = me._adapter;
var options = me.options;
var formats = options.time.displayFormats;
var minorFormat = formats[me._unit];
var majorUnit = me._majorUnit;
var majorFormat = formats[majorUnit];
var tick = ticks[index];
var tickOpts = options.ticks;
var major = majorUnit && majorFormat && tick && tick.major;
var label = adapter.format(time, format ? format : major ? majorFormat : minorFormat);
var nestedTickOpts = major ? tickOpts.major : tickOpts.minor;
var formatter = resolve([
nestedTickOpts.callback,
nestedTickOpts.userCallback,
tickOpts.callback,
tickOpts.userCallback
]);
return formatter ? formatter(label, index, ticks) : label;
},
convertTicksToLabels: function(ticks) {
var labels = [];
var i, ilen;
for (i = 0, ilen = ticks.length; i < ilen; ++i) {
labels.push(this.tickFormatFunction(ticks[i].value, i, ticks));
}
return labels;
},
/**
* @private
*/
getPixelForOffset: function(time) {
var me = this;
var offsets = me._offsets;
var pos = interpolate(me._table, 'time', time, 'pos');
return me.getPixelForDecimal((offsets.start + pos) * offsets.factor);
},
getPixelForValue: function(value, index, datasetIndex) {
var me = this;
var time = null;
if (index !== undefined && datasetIndex !== undefined) {
time = me._timestamps.datasets[datasetIndex][index];
}
if (time === null) {
time = parse(me, value);
}
if (time !== null) {
return me.getPixelForOffset(time);
}
},
getPixelForTick: function(index) {
var ticks = this.getTicks();
return index >= 0 && index < ticks.length ?
this.getPixelForOffset(ticks[index].value) :
null;
},
getValueForPixel: function(pixel) {
var me = this;
var offsets = me._offsets;
var pos = me.getDecimalForPixel(pixel) / offsets.factor - offsets.end;
var time = interpolate(me._table, 'pos', pos, 'time');
// DEPRECATION, we should return time directly
return me._adapter._create(time);
},
/**
* @private
*/
_getLabelSize: function(label) {
var me = this;
var ticksOpts = me.options.ticks;
var tickLabelWidth = me.ctx.measureText(label).width;
var angle = helpers.toRadians(me.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation);
var cosRotation = Math.cos(angle);
var sinRotation = Math.sin(angle);
var tickFontSize = valueOrDefault(ticksOpts.fontSize, defaults.global.defaultFontSize);
return {
w: (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation),
h: (tickLabelWidth * sinRotation) + (tickFontSize * cosRotation)
};
},
/**
* Crude approximation of what the label width might be
* @private
*/
getLabelWidth: function(label) {
return this._getLabelSize(label).w;
},
/**
* @private
*/
getLabelCapacity: function(exampleTime) {
var me = this;
var timeOpts = me.options.time;
var displayFormats = timeOpts.displayFormats;
// pick the longest format (milliseconds) for guestimation
var format = displayFormats[timeOpts.unit] || displayFormats.millisecond;
var exampleLabel = me.tickFormatFunction(exampleTime, 0, ticksFromTimestamps(me, [exampleTime], me._majorUnit), format);
var size = me._getLabelSize(exampleLabel);
var capacity = Math.floor(me.isHorizontal() ? me.width / size.w : me.height / size.h);
if (me.options.offset) {
capacity--;
}
return capacity > 0 ? capacity : 1;
}
});
// INTERNAL: static default options, registered in src/index.js
module.exports._defaults = defaultConfig;