Как добавить нулевые значения в временные ряды в 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 и отправлю сообщение.