Я начал с создания скрипки , которая иллюстрирует проблему. Интересно, что здесь есть меню выбора, которое показывает, какие временные интервалы соответствуют данным и уровню масштабирования (области) диаграммы.
Неверно показывать больше, чем ширина / 2 точки (так как они не будет отображаться), и также нецелесообразно показывать менее двух точек, поэтому «неподходящими» параметрами являются серые италийские c:
Он использует имена интервалов сопоставления объектов с количеством миллисекунд в соответствующем интервале d3:
const intervals = {
timeSecond: 1000,
timeMinute: 60000,
timeHour: 3600000,
timeDay: 86400000,
timeWeek: 604800000,
timeMonth: 2628000000,
timeYear: 31536000000
}
allowed_intervals
определяет первый и последний соответствующий интервал:
function allowed_intervals(chart, intervals, dateDomain) {
const dt = dateDomain[1].getTime() - dateDomain[0].getTime(),
first = Object.entries(intervals).find(
([iname, ms]) => dt / ms < chart.width() / 2);
if(!first)
throw new Error('date range too long')
const last = Object.entries(intervals).reverse().find(
([iname, ms]) => d3[iname](dateDomain[0]).getTime() !== d3[iname](dateDomain[1]).getTime());
return [first[0],last[0]];
}
Так что все хорошо. В примере печатаются результирующие данные, и мы можем видеть, что если мы заполним данные примера d3.timeMinute
, то получим 332482 точки данных из исходных 15. Это явно слишком много данных, особенно для простого примера.
Это хороший алгоритм для поиска подходящего временного интервала d3. Однако происходит сбой, когда мы включаем масштабирование, потому что теперь мы можем увеличить масштаб за один час, скажем, где timeMinute
подходит, но если вы используете этот интервал для всех данных, это слишком много точек, и диаграмма замедляется до остановка.
Поэтому я начал думать о том, как сделать его более эффективным. Нам не нужно заполнять каждый пропущенный интервал времени. Нам действительно нужно убедиться, что мы поймали передний фронт , когда данные переходят от ненулевого к нулю, и передний фронт , где данные идут от нуля до ненулевого , В этих случаях нам нужно только добавить нули к входным данным.
Вот новая версия fill_composite_intervals
, которая использует нарастающие и падающие края, добавляя только столько нулей, сколько необходимо для отображения этих краев:
// input: a group with keys [category, time] and numeric values; a d3 time interval
// output: the same, but with zeroes filled in per the interval
function fill_composite_intervals(group, interval) {
return {
all: function() {
const retVal = [];
const allArray = group.all().slice();
if (!allArray.length) {
return retVal;
}
// make sure input data is sorted
allArray.sort((a, b) => a.key[1].getTime() - b.key[1].getTime());
// find all time intervals within the data
// pad at both ends to add leading and trailing zeros
const target = interval.range(interval.offset(allArray[0].key[1], -1),
interval.offset(allArray[allArray.length-1].key[1], 2));
// separate the data for each category
const allMap = new Map();
allArray.forEach(({key: [cat, time], value}) => {
let innerArray = allMap.get(cat);
if (!innerArray) {
innerArray = [];
allMap.set(cat, innerArray);
}
innerArray.push({key: time, value});
});
// walk each category, adding leading and trailing zeros
allMap.forEach((value, key, map) => {
const orig = value.map(({key, value}) => ({key: new Date(key), value}));
const result = [];
if (orig.length) {
let oi = 0, ti = 0, last_filled = false, skipped_fill = false;
while(oi < orig.length && ti < target.length) {
if (orig[oi].key <= target[ti]) {
if(skipped_fill) {
// in the last iteration, we skipped a zero
// so add one now (rising edge)
result.push({key: target[ti-1], value: 0});
skipped_fill = false;
}
result.push(orig[oi]);
if (orig[oi++].key.getTime() === target[ti].getTime()) {
++ti;
}
last_filled = false;
} else {
if(!last_filled) {
// last iteration we pushed a value
// so push a zero now (falling edge)
result.push({key: target[ti], value: 0});
last_filled = true;
}
else skipped_fill = true;
++ti;
}
}
if (oi<orig.length) {
Array.prototype.push.apply(result, orig.slice(oi));
}
if (ti<target.length) {
// add one trailing zero at the end
result.push({key: target[ti], value: 0});
}
}
map.set(key, result);
});
allMap.forEach((value, key, map) => {
value.forEach(({key: time, value}) => {
retVal.push({
key: [key, time],
value
});
});
});
return retVal;
}
};
}
См. Комментарии в код для объяснения. Он производит только данные, пропорциональные входным данным, например, 67 точек для ввода 15 с timeMinute
вместо 300 + K.
Интересно, что я обнаружил, что d3.curveCardinal
создает странные артефакты, когда меньше нулей , Интуитивно я думаю, что линия набирает слишком много «импульса», если пропустить точки. Поэтому я выбрал d3.curveMonotoneX . Я думаю, что в любом случае это более уместно.
.curve(d3.curveMonotoneX)
Я также добавил interval.range
в начале и в конце, чтобы данные начинались и заканчивались на нуле, что более привлекательно.
Этот пример все еще медленный, когда вы выбираете d3.timeSecond
(он все еще проходит через 300 + K точек), но, кажется, он работает нормально до timeMinute
, что, кажется, отражает разрешение этих данных.
Дальнейшие возможные улучшения:
- добавьте больше начальных и конечных нулей, чтобы кривые были согласованными / симметричными c
- , прекратите использование
interval.range
, чтобы вычислять не так много точек и выброшенный; вместо этого обнаруживайте нарастающие и падающие фронты, используя interval.offset
и только следующие / последние точки данных (хитро!)
Пример скрипта .