mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-06 16:26:52 +01:00
Split time scale into time and timeseries (#7525)
* Split time scale into time and timeseries * Address comment
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
title: Time Cartesian Axis
|
||||
---
|
||||
|
||||
The time scale is used to display times and dates. When building its ticks, it will automatically calculate the most comfortable unit base on the size of the scale.
|
||||
The time scale is used to display times and dates. Data are spread according to the amount of time between data points. When building its ticks, it will automatically calculate the most comfortable unit base on the size of the scale.
|
||||
|
||||
## Date Adapters
|
||||
|
||||
@@ -25,7 +25,6 @@ The following options are provided by the time scale. You may also set options p
|
||||
| Name | Type | Default | Description
|
||||
| ---- | ---- | ------- | -----------
|
||||
| `adapters.date` | `object` | `{}` | Options for adapter for external date library if that adapter needs or supports options
|
||||
| `distribution` | `string` | `'linear'` | How data is plotted. [more...](#scale-distribution)
|
||||
| `bounds` | `string` | `'data'` | Determines the scale bounds. [more...](#scale-bounds)
|
||||
| `ticks.source` | `string` | `'auto'` | How ticks are generated. [more...](#ticks-source)
|
||||
| `time.displayFormats` | `object` | | Sets how different time units are displayed. [more...](#display-formats)
|
||||
@@ -107,30 +106,6 @@ var chart = new Chart(ctx, {
|
||||
});
|
||||
```
|
||||
|
||||
### Scale Distribution
|
||||
|
||||
The `distribution` property controls the 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
|
||||
|
||||
```javascript
|
||||
var chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
distribution: 'series'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
When the scale is in `series` mode, the data indices are expected to be unique, sorted, and consistent across datasets.
|
||||
|
||||
### Scale Bounds
|
||||
|
||||
The `bounds` property controls the scale boundary strategy (bypassed by `min`/`max` time options).
|
||||
|
||||
25
docs/docs/axes/cartesian/timeseries.md
Normal file
25
docs/docs/axes/cartesian/timeseries.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Time Series Axis
|
||||
---
|
||||
|
||||
The time series scale extends from the time scale and supports all the same options. However, for the time series scale, each data point is spread equidistant. Also, the data indices are expected to be unique, sorted, and consistent across datasets.
|
||||
|
||||
## Example
|
||||
|
||||
```javascript
|
||||
var chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'timeseries',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## More details
|
||||
|
||||
Please see [the time scale documentation](./time.md) for all other details.
|
||||
@@ -156,6 +156,8 @@ options: {
|
||||
}
|
||||
```
|
||||
|
||||
Also, the time scale option `distribution: 'series'` was removed and a new scale type `timeseries` was introduced in its place.
|
||||
|
||||
#### Animations
|
||||
|
||||
Animation system was completely rewritten in Chart.js v3. Each property can now be animated separately. Please see [animations](../configuration/animations.mdx) docs for details.
|
||||
|
||||
@@ -162,3 +162,25 @@ export function unlistenArrayEvents(array, listener) {
|
||||
|
||||
delete array._chartjs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} items
|
||||
*/
|
||||
export function _arrayUnique(items) {
|
||||
const set = new Set();
|
||||
let i, ilen;
|
||||
|
||||
for (i = 0, ilen = items.length; i < ilen; ++i) {
|
||||
set.add(items[i]);
|
||||
}
|
||||
|
||||
if (set.size === ilen) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const result = [];
|
||||
set.forEach(item => {
|
||||
result.push(item);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ export {default as LinearScale} from './scale.linear';
|
||||
export {default as LogarithmicScale} from './scale.logarithmic';
|
||||
export {default as RadialLinearScale} from './scale.radialLinear';
|
||||
export {default as TimeScale} from './scale.time';
|
||||
export {default as TimeSeriesScale} from './scale.timeseries';
|
||||
|
||||
@@ -2,7 +2,7 @@ import adapters from '../core/core.adapters';
|
||||
import {isFinite, isNullOrUndef, mergeIf, valueOrDefault} from '../helpers/helpers.core';
|
||||
import {toRadians} from '../helpers/helpers.math';
|
||||
import Scale from '../core/core.scale';
|
||||
import {_filterBetween, _lookup, _lookupByKey} from '../helpers/helpers.collection';
|
||||
import {_arrayUnique, _filterBetween, _lookup, _lookupByKey} from '../helpers/helpers.collection';
|
||||
|
||||
/**
|
||||
* @typedef { import("../core/core.adapters").Unit } Unit
|
||||
@@ -40,31 +40,10 @@ function sorter(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} items
|
||||
*/
|
||||
function arrayUnique(items) {
|
||||
const set = new Set();
|
||||
let i, ilen;
|
||||
|
||||
for (i = 0, ilen = items.length; i < ilen; ++i) {
|
||||
set.add(items[i]);
|
||||
}
|
||||
|
||||
if (set.size === ilen) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const result = [];
|
||||
set.forEach(item => {
|
||||
result.push(item);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TimeScale} scale
|
||||
* @param {*} input
|
||||
* @return {number}
|
||||
*/
|
||||
function parse(scale, input) {
|
||||
if (isNullOrUndef(input)) {
|
||||
@@ -100,133 +79,6 @@ function parse(scale, input) {
|
||||
return +value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TimeScale} scale
|
||||
*/
|
||||
function getDataTimestamps(scale) {
|
||||
const isSeries = scale.options.distribution === 'series';
|
||||
let timestamps = scale._cache.data || [];
|
||||
let i, ilen;
|
||||
|
||||
if (timestamps.length) {
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
const metas = scale.getMatchingVisibleMetas();
|
||||
|
||||
if (isSeries && metas.length) {
|
||||
return metas[0].controller.getAllParsedValues(scale);
|
||||
}
|
||||
|
||||
for (i = 0, ilen = metas.length; i < ilen; ++i) {
|
||||
timestamps = timestamps.concat(metas[i].controller.getAllParsedValues(scale));
|
||||
}
|
||||
|
||||
// We can not assume data is in order or unique - not even for single dataset
|
||||
// It seems to be somewhat faster to do sorting first
|
||||
return (scale._cache.data = arrayUnique(timestamps.sort(sorter)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TimeScale} scale
|
||||
*/
|
||||
function getLabelTimestamps(scale) {
|
||||
const isSeries = scale.options.distribution === 'series';
|
||||
const timestamps = scale._cache.labels || [];
|
||||
let i, ilen;
|
||||
|
||||
if (timestamps.length) {
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
const labels = scale.getLabels();
|
||||
for (i = 0, ilen = labels.length; i < ilen; ++i) {
|
||||
timestamps.push(parse(scale, labels[i]));
|
||||
}
|
||||
|
||||
// We could assume labels are in order and unique - but let's not
|
||||
return (scale._cache.labels = isSeries ? timestamps : arrayUnique(timestamps.sort(sorter)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TimeScale} scale
|
||||
*/
|
||||
function getAllTimestamps(scale) {
|
||||
let timestamps = scale._cache.all || [];
|
||||
|
||||
if (timestamps.length) {
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
const data = getDataTimestamps(scale);
|
||||
const label = getLabelTimestamps(scale);
|
||||
if (data.length && label.length) {
|
||||
// If combining labels and data (data might not contain all labels),
|
||||
// we need to recheck uniqueness and sort
|
||||
timestamps = arrayUnique(data.concat(label).sort(sorter));
|
||||
} else {
|
||||
timestamps = data.length ? data : label;
|
||||
}
|
||||
timestamps = scale._cache.all = timestamps;
|
||||
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {number[]} timestamps
|
||||
* @param {number} min
|
||||
* @param {number} max
|
||||
* @param {string} distribution
|
||||
* @return {object[]}
|
||||
*/
|
||||
function buildLookupTable(timestamps, min, max, distribution) {
|
||||
if (distribution === 'linear' || !timestamps.length) {
|
||||
return [
|
||||
{time: min, pos: 0},
|
||||
{time: max, pos: 1}
|
||||
];
|
||||
}
|
||||
|
||||
const table = [];
|
||||
const items = [min];
|
||||
let 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')
|
||||
@@ -321,64 +173,6 @@ function addTick(timestamps, ticks, time) {
|
||||
ticks[timestamp] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {TimeScale} scale
|
||||
*/
|
||||
function generate(scale) {
|
||||
const adapter = scale._adapter;
|
||||
const min = scale.min;
|
||||
const max = scale.max;
|
||||
const options = scale.options;
|
||||
const timeOpts = options.time;
|
||||
// @ts-ignore
|
||||
const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, scale._getLabelCapacity(min));
|
||||
const stepSize = valueOrDefault(timeOpts.stepSize, 1);
|
||||
const weekday = minor === 'week' ? timeOpts.isoWeekday : false;
|
||||
const ticks = {};
|
||||
let first = min;
|
||||
let 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 new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor);
|
||||
}
|
||||
|
||||
if (scale.options.ticks.source === 'data') {
|
||||
// need to make sure ticks are in data in this case
|
||||
const timestamps = getDataTimestamps(scale);
|
||||
|
||||
for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) {
|
||||
addTick(timestamps, ticks, time);
|
||||
}
|
||||
|
||||
if (time === max || options.bounds === 'ticks') {
|
||||
addTick(timestamps, ticks, time);
|
||||
}
|
||||
} else {
|
||||
for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) {
|
||||
ticks[time] = true;
|
||||
}
|
||||
|
||||
if (time === max || options.bounds === 'ticks') {
|
||||
ticks[time] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(ticks).map(x => +x);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -464,50 +258,7 @@ function ticksFromTimestamps(scale, values, majorUnit) {
|
||||
return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TimeScale} scale
|
||||
*/
|
||||
function getTimestampsForTicks(scale) {
|
||||
if (scale.options.ticks.source === 'labels') {
|
||||
return getLabelTimestamps(scale);
|
||||
}
|
||||
|
||||
return generate(scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TimeScale} scale
|
||||
*/
|
||||
function getTimestampsForTable(scale) {
|
||||
return scale.options.distribution === 'series'
|
||||
? getAllTimestamps(scale)
|
||||
: [scale.min, scale.max];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TimeScale} scale
|
||||
*/
|
||||
function getLabelBounds(scale) {
|
||||
const arr = getLabelTimestamps(scale);
|
||||
let min = Number.POSITIVE_INFINITY;
|
||||
let max = Number.NEGATIVE_INFINITY;
|
||||
|
||||
if (arr.length) {
|
||||
min = arr[0];
|
||||
max = arr[arr.length - 1];
|
||||
}
|
||||
return {min, max};
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
/**
|
||||
* 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)
|
||||
@@ -586,7 +337,7 @@ class TimeScale extends Scale {
|
||||
|
||||
/**
|
||||
* @param {*} raw
|
||||
* @param {number} index
|
||||
* @param {number?} [index]
|
||||
* @return {number}
|
||||
*/
|
||||
parse(raw, index) { // eslint-disable-line no-unused-vars
|
||||
@@ -604,6 +355,13 @@ class TimeScale extends Scale {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
getTimestampsForTable() {
|
||||
return [this.min, this.max];
|
||||
}
|
||||
|
||||
determineDataLimits() {
|
||||
const me = this;
|
||||
const options = me.options;
|
||||
@@ -627,7 +385,7 @@ class TimeScale extends Scale {
|
||||
// If we have user provided `min` and `max` labels / data bounds can be ignored
|
||||
if (!minDefined || !maxDefined) {
|
||||
// Labels are always considered, when user did not force bounds
|
||||
_applyBounds(getLabelBounds(me));
|
||||
_applyBounds(me._getLabelBounds());
|
||||
|
||||
// If `bounds` is `'ticks'` and `ticks.source` is `'labels'`,
|
||||
// data bounds are ignored (and don't need to be determined)
|
||||
@@ -644,6 +402,21 @@ class TimeScale extends Scale {
|
||||
me.max = Math.max(min + 1, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getLabelBounds() {
|
||||
const arr = this.getLabelTimestamps();
|
||||
let min = Number.POSITIVE_INFINITY;
|
||||
let max = Number.NEGATIVE_INFINITY;
|
||||
|
||||
if (arr.length) {
|
||||
min = arr[0];
|
||||
max = arr[arr.length - 1];
|
||||
}
|
||||
return {min, max};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {object[]}
|
||||
*/
|
||||
@@ -652,8 +425,7 @@ class TimeScale extends Scale {
|
||||
const options = me.options;
|
||||
const timeOpts = options.time;
|
||||
const tickOpts = options.ticks;
|
||||
const distribution = options.distribution;
|
||||
const timestamps = getTimestampsForTicks(me);
|
||||
const timestamps = tickOpts.source === 'labels' ? me.getLabelTimestamps() : me._generate();
|
||||
|
||||
if (options.bounds === 'ticks' && timestamps.length) {
|
||||
me.min = me._userMin || timestamps[0];
|
||||
@@ -673,7 +445,7 @@ class TimeScale extends Scale {
|
||||
: 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(getTimestampsForTable(me), min, max, distribution);
|
||||
me._table = me.buildLookupTable(me.getTimestampsForTable(), min, max);
|
||||
me._offsets = computeOffsets(me._table, timestamps, min, max, options);
|
||||
|
||||
if (options.reverse) {
|
||||
@@ -683,6 +455,86 @@ class TimeScale extends Scale {
|
||||
return ticksFromTimestamps(me, ticks, me._majorUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @private
|
||||
*/
|
||||
_generate() {
|
||||
const me = this;
|
||||
const adapter = me._adapter;
|
||||
const min = me.min;
|
||||
const max = me.max;
|
||||
const options = me.options;
|
||||
const timeOpts = options.time;
|
||||
// @ts-ignore
|
||||
const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, me._getLabelCapacity(min));
|
||||
const stepSize = valueOrDefault(timeOpts.stepSize, 1);
|
||||
const weekday = minor === 'week' ? timeOpts.isoWeekday : false;
|
||||
const ticks = {};
|
||||
let first = min;
|
||||
let 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 new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor);
|
||||
}
|
||||
|
||||
if (me.options.ticks.source === 'data') {
|
||||
// need to make sure ticks are in data in this case
|
||||
const timestamps = me.getDataTimestamps();
|
||||
|
||||
for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) {
|
||||
addTick(timestamps, ticks, time);
|
||||
}
|
||||
|
||||
if (time === max || options.bounds === 'ticks') {
|
||||
addTick(timestamps, ticks, time);
|
||||
}
|
||||
} else {
|
||||
for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) {
|
||||
ticks[time] = true;
|
||||
}
|
||||
|
||||
if (time === max || options.bounds === 'ticks') {
|
||||
ticks[time] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(ticks).map(x => +x);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {number} min
|
||||
* @param {number} max
|
||||
* @return {object[]}
|
||||
* @protected
|
||||
*/
|
||||
buildLookupTable(timestamps, min, max) {
|
||||
return [
|
||||
{time: min, pos: 0},
|
||||
{time: max, pos: 1}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @return {string}
|
||||
@@ -795,6 +647,50 @@ class TimeScale extends Scale {
|
||||
const capacity = Math.floor(me.isHorizontal() ? me.width / size.w : me.height / size.h) - 1;
|
||||
return capacity > 0 ? capacity : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
getDataTimestamps() {
|
||||
const me = this;
|
||||
let timestamps = me._cache.data || [];
|
||||
let i, ilen;
|
||||
|
||||
if (timestamps.length) {
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
const metas = me.getMatchingVisibleMetas();
|
||||
|
||||
for (i = 0, ilen = metas.length; i < ilen; ++i) {
|
||||
timestamps = timestamps.concat(metas[i].controller.getAllParsedValues(me));
|
||||
}
|
||||
|
||||
// We can not assume data is in order or unique - not even for single dataset
|
||||
// It seems to be somewhat faster to do sorting first
|
||||
return (me._cache.data = _arrayUnique(timestamps.sort(sorter)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
getLabelTimestamps() {
|
||||
const me = this;
|
||||
const timestamps = me._cache.labels || [];
|
||||
let i, ilen;
|
||||
|
||||
if (timestamps.length) {
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
const labels = me.getLabels();
|
||||
for (i = 0, ilen = labels.length; i < ilen; ++i) {
|
||||
timestamps.push(parse(me, labels[i]));
|
||||
}
|
||||
|
||||
// We could assume labels are in order and unique - but let's not
|
||||
return (me._cache.labels = _arrayUnique(timestamps.sort(sorter)));
|
||||
}
|
||||
}
|
||||
|
||||
TimeScale.id = 'time';
|
||||
|
||||
131
src/scales/scale.timeseries.js
Normal file
131
src/scales/scale.timeseries.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import TimeScale from './scale.time';
|
||||
import {_arrayUnique} from '../helpers/helpers.collection';
|
||||
|
||||
/**
|
||||
* @param {number} a
|
||||
* @param {number} b
|
||||
*/
|
||||
function sorter(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
|
||||
class TimeSeriesScale extends TimeScale {
|
||||
|
||||
/**
|
||||
* Returns all timestamps
|
||||
* @protected
|
||||
*/
|
||||
getTimestampsForTable() {
|
||||
const me = this;
|
||||
let timestamps = me._cache.all || [];
|
||||
|
||||
if (timestamps.length) {
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
const data = me.getDataTimestamps();
|
||||
const label = me.getLabelTimestamps();
|
||||
if (data.length && label.length) {
|
||||
// If combining labels and data (data might not contain all labels),
|
||||
// we need to recheck uniqueness and sort
|
||||
timestamps = _arrayUnique(data.concat(label).sort(sorter));
|
||||
} else {
|
||||
timestamps = data.length ? data : label;
|
||||
}
|
||||
timestamps = me._cache.all = timestamps;
|
||||
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {number} min
|
||||
* @param {number} max
|
||||
* @return {object[]}
|
||||
* @protected
|
||||
*/
|
||||
buildLookupTable(timestamps, min, max) {
|
||||
if (!timestamps.length) {
|
||||
return [
|
||||
{time: min, pos: 0},
|
||||
{time: max, pos: 1}
|
||||
];
|
||||
}
|
||||
|
||||
const table = [];
|
||||
const items = [min];
|
||||
let 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
getDataTimestamps() {
|
||||
const me = this;
|
||||
const timestamps = me._cache.data || [];
|
||||
|
||||
if (timestamps.length) {
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
const metas = me.getMatchingVisibleMetas();
|
||||
return (me._cache.data = metas.length ? metas[0].controller.getAllParsedValues(me) : []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
getLabelTimestamps() {
|
||||
const me = this;
|
||||
const timestamps = me._cache.labels || [];
|
||||
let i, ilen;
|
||||
|
||||
if (timestamps.length) {
|
||||
return timestamps;
|
||||
}
|
||||
|
||||
const labels = me.getLabels();
|
||||
for (i = 0, ilen = labels.length; i < ilen; ++i) {
|
||||
timestamps.push(me.parse(labels[i]));
|
||||
}
|
||||
|
||||
// We could assume labels are in order and unique - but let's not
|
||||
return (me._cache.labels = timestamps);
|
||||
}
|
||||
}
|
||||
|
||||
TimeSeriesScale.id = 'timeseries';
|
||||
|
||||
// INTERNAL: default options, registered in src/index.js
|
||||
TimeSeriesScale.defaults = TimeScale.defaults;
|
||||
|
||||
export default TimeSeriesScale;
|
||||
@@ -9,15 +9,14 @@ module.exports = {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
time: {
|
||||
parser: 'YYYY',
|
||||
unit: 'year'
|
||||
},
|
||||
ticks: {
|
||||
source: 'auto'
|
||||
},
|
||||
distribution: 'series'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: false
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
min: '2012',
|
||||
max: '2051',
|
||||
offset: true,
|
||||
@@ -18,8 +18,7 @@ module.exports = {
|
||||
},
|
||||
ticks: {
|
||||
source: 'data'
|
||||
},
|
||||
distribution: 'series'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: false
|
||||
|
||||
@@ -9,15 +9,14 @@ module.exports = {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
time: {
|
||||
parser: 'YYYY',
|
||||
unit: 'year'
|
||||
},
|
||||
ticks: {
|
||||
source: 'data'
|
||||
},
|
||||
distribution: 'series'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: false
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
min: '2012',
|
||||
max: '2051',
|
||||
offset: true,
|
||||
@@ -18,8 +18,7 @@ module.exports = {
|
||||
},
|
||||
ticks: {
|
||||
source: 'labels'
|
||||
},
|
||||
distribution: 'series'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: false
|
||||
|
||||
@@ -9,15 +9,14 @@ module.exports = {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
time: {
|
||||
parser: 'YYYY',
|
||||
unit: 'year'
|
||||
},
|
||||
ticks: {
|
||||
source: 'labels'
|
||||
},
|
||||
distribution: 'series'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: false
|
||||
|
||||
@@ -9,12 +9,11 @@ module.exports = {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
max: '2050',
|
||||
time: {
|
||||
parser: 'YYYY'
|
||||
},
|
||||
distribution: 'series',
|
||||
reverse: true,
|
||||
ticks: {
|
||||
source: 'labels'
|
||||
|
||||
@@ -9,13 +9,12 @@ module.exports = {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
min: '2012',
|
||||
max: '2050',
|
||||
time: {
|
||||
parser: 'YYYY'
|
||||
},
|
||||
distribution: 'series',
|
||||
reverse: true,
|
||||
ticks: {
|
||||
source: 'labels'
|
||||
|
||||
@@ -9,12 +9,11 @@ module.exports = {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
min: '2012',
|
||||
time: {
|
||||
parser: 'YYYY'
|
||||
},
|
||||
distribution: 'series',
|
||||
reverse: true,
|
||||
ticks: {
|
||||
source: 'labels'
|
||||
|
||||
@@ -9,11 +9,10 @@ module.exports = {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
time: {
|
||||
parser: 'YYYY'
|
||||
},
|
||||
distribution: 'series',
|
||||
reverse: true,
|
||||
ticks: {
|
||||
source: 'labels'
|
||||
|
||||
@@ -79,7 +79,6 @@ describe('Time scale tests', function() {
|
||||
beginAtZero: false,
|
||||
scaleLabel: Chart.defaults.scale.scaleLabel,
|
||||
bounds: 'data',
|
||||
distribution: 'linear',
|
||||
adapters: {},
|
||||
ticks: {
|
||||
minRotation: 0,
|
||||
@@ -786,8 +785,8 @@ describe('Time scale tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when distribution', function() {
|
||||
describe('is "series"', function() {
|
||||
describe('when scale type', function() {
|
||||
describe('is "timeseries"', function() {
|
||||
beforeEach(function() {
|
||||
this.chart = window.acquireChart({
|
||||
type: 'line',
|
||||
@@ -798,11 +797,10 @@ describe('Time scale tests', function() {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: 'timeseries',
|
||||
time: {
|
||||
parser: 'YYYY'
|
||||
},
|
||||
distribution: 'series',
|
||||
ticks: {
|
||||
source: 'labels'
|
||||
}
|
||||
@@ -870,7 +868,7 @@ describe('Time scale tests', function() {
|
||||
expect(scale.getPixelForValue(moment('2042').valueOf())).toBeCloseToPixel(start + slice * 5);
|
||||
});
|
||||
});
|
||||
describe('is "linear"', function() {
|
||||
describe('is "time"', function() {
|
||||
beforeEach(function() {
|
||||
this.chart = window.acquireChart({
|
||||
type: 'line',
|
||||
@@ -885,7 +883,6 @@ describe('Time scale tests', function() {
|
||||
time: {
|
||||
parser: 'YYYY'
|
||||
},
|
||||
distribution: 'linear',
|
||||
ticks: {
|
||||
source: 'labels'
|
||||
}
|
||||
@@ -1087,8 +1084,8 @@ describe('Time scale tests', function() {
|
||||
});
|
||||
|
||||
['auto', 'data', 'labels'].forEach(function(source) {
|
||||
['series', 'linear'].forEach(function(distribution) {
|
||||
describe('when ticks.source is "' + source + '" and distribution is "' + distribution + '"', function() {
|
||||
['timeseries', 'time'].forEach(function(type) {
|
||||
describe('when ticks.source is "' + source + '" and scale type is "' + type + '"', function() {
|
||||
beforeEach(function() {
|
||||
this.chart = window.acquireChart({
|
||||
type: 'line',
|
||||
@@ -1099,15 +1096,14 @@ describe('Time scale tests', function() {
|
||||
options: {
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
type: type,
|
||||
time: {
|
||||
parser: 'YYYY',
|
||||
unit: 'year'
|
||||
},
|
||||
ticks: {
|
||||
source: source
|
||||
},
|
||||
distribution: distribution
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1154,8 +1150,8 @@ describe('Time scale tests', function() {
|
||||
});
|
||||
|
||||
['data', 'labels'].forEach(function(source) {
|
||||
['series', 'linear'].forEach(function(distribution) {
|
||||
describe('when ticks.source is "' + source + '" and distribution is "' + distribution + '"', function() {
|
||||
['timeseries', 'time'].forEach(function(type) {
|
||||
describe('when ticks.source is "' + source + '" and scale type is "' + type + '"', function() {
|
||||
beforeEach(function() {
|
||||
this.chart = window.acquireChart({
|
||||
type: 'line',
|
||||
@@ -1167,14 +1163,13 @@ describe('Time scale tests', function() {
|
||||
scales: {
|
||||
x: {
|
||||
id: 'x',
|
||||
type: 'time',
|
||||
type: type,
|
||||
time: {
|
||||
parser: 'YYYY'
|
||||
},
|
||||
ticks: {
|
||||
source: source
|
||||
},
|
||||
distribution: distribution
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user