Как добавить нулевые значения в временные ряды в d3.js/JavaScript
Это проблема, которую я ранее решал с помощью php или (что я думал) ненужно сложных запросов MySQL, но я вдруг подумал, что в JavaScript/d3.js должно быть более элегантное решение.
Представьте, что у меня есть набор дат и значений, которые я хочу преобразовать в гистограмму в d3.js.
date,value
2013-01,53
2013-02,165
2013-03,269
2013-04,344
2013-05,376
2013-06,410
2013-07,421
2013-09,376
2013-10,359
2013-11,392
2013-12,433
2014-01,455
2014-02,478
Вы заметите, что в течение 8-го месяца (август) в данных нет записи. Предполагается, что август является нулевым значением, а конечным результатом является то, что итоговая диаграмма выглядит нормально, но, конечно, 8-го месяца, где она должна быть пробелом (ноль).
![enter image description here]()
![enter image description here]()
У меня есть jsfiddle script и данные здесь для справки.
Я попытался добавить полный набор данных, заполненный нулями, а затем повторить его, чтобы включить значения из данных, но это тоже слишком сложно. Я исхожу из предположения, что есть изящное решение, о котором я слишком незнаю, чтобы знать.
Спасибо за вашу помощь.
Изменить # 1: ответ на ответ от explunit:
Решение в идеале должно быть манипуляцией с серией данных, а не только с гравюрой. Это означало бы, что эквивалент линейного графика в этот jsfiddle имел бы внезапное падение в середине его.
Редактировать # 2: После немного воспроизведения:
После игры с предложениями на странице групп Google здесь, мне удалось получить фрагмент кода, чтобы сделать то, что я находясь в поиске. Он принимает данные временной метки, создает домен на основе временного диапазона и создает отдельный массив с отдельными месяцами (в данном случае). Затем я грубо перебираю оба набора массивов и добавляю значения, соответствующие исходному (не полностью заполненному значениям времени) массиву, в массив со всеми значениями времени (и значениями данных, первоначально установленными на ноль).
Конечным результатом является линейный график, который изначально выглядел бы так, потому что он выполняет итерацию между июлем и сентябрем 2013 года;
![enter image description here]()
Впоследствии создается как это, потому что значение августа будет добавлено как ноль;
![enter image description here]()
Здесь jsfiddle этого кода;
Я сначала скажу это. Хотя он выполняет работу, которую я желаю в этом случае, это долгий путь от элегантного или расширяемого. Если кто-то умнее меня сможет увидеть, как это можно сделать менее оскорбительным, я был бы признателен.
Ответы
Ответ 1
Я не очень улучшил общий подход, но если вы используете еще несколько встроенных методов и добавьте подчеркивание /lodash, вы можете сделать преобразование данных намного короче:
x.domain(d3.extent(data, function(d) { return d.date; })).ticks(d3.time.month);
y.domain([0, d3.max(data, function(d) { return d.value; })]);
var newData = x.ticks().map(function(monthBucket) {
return _.find(data, {date: monthBucket}) || {date: monthBucket, value: 0};
});
Если мы скажем, что он должен использовать месячные тики, мы можем просто вернуть массив тиков, а не создать отдельный массив ведер.
И затем с этой точки мы просто используем .map
, а не for
метод loop и lodash (или подчеркивание) _.find
, чтобы соответствовать нашим исходным данным.
Обновленная скрипка здесь: http://jsfiddle.net/a5jUz/3/
Оригинальный ответ ниже... в случае, если вы хотите использовать шкалы D3 для распространения значений на гистограмме:
1 - Вы должны использовать шкалу времени, а не порядковый масштаб:
var x = d3.time.scale().range([0, width]);
2 - вам необходимо установить домен этого масштаба на основе минимального/максимального диапазона дат:
x.domain(d3.extent(data, function(d) { return d.date; })).nice();
3 - [уродливая часть] теперь, когда вы не используете порядковый масштаб, у вас нет функции rangeBand
для позиционирования бара:
// TODO: calculate based on overall width & number of data points
.attr("x", function(d) { return x(d.date); })
.attr("width", 16)
Обновлена скрипка здесь:
http://jsfiddle.net/LWyjf/
Ответ 2
Вот еще один вариант для нулей заполнения без использования lodash/underscore, используя d3.get()
, а не _.find()
. Не уверен, как это влияет на производительность.
var date_range = d3.time.hours(startDate, endDate, 1);
var m = d3.map(data, function(d) { return d.date });
var newData = date_range.map(function(bucket) {
return m.get(bucket) || {date: bucket, value: 0};
});
Ответ 3
Чтобы улучшить ответ @explunit, я предпочитаю заполнять нули перед сопоставлением данных в диапазоне доменов, чтобы получить полный набор данных, который не повлияет на изменения масштаба в домене:
var date_range = d3.time.days(minX, maxX, 1);
var newData = date_range.map(function(dayBucket) {
return _.find(data, function(d) {
return d.date = dayBucket;
} || {date: dayBucket, value: 0};
});
а затем
x.domain(d3.extent(newData, function(d) { return d.date; })).ticks(d3.time.day);
y.domain([0, d3.max(newData, function(d) { return d.value; })]);
и др.
Я скоро обновлю JSFiddle и отправлю сообщение.