dc.js Упорядочение оси X по месяцам

Я пытаюсь создать столбчатую диаграмму с накоплением, используя dc.js следующим образом:

Данные:

<pre id="data">
Status,StatusT,Phase,PhaseT,CompletionDate,CalMonth,Sales,SalesCount
3,Won,Z04,Decide,20130101,201012,2203262.13,1
3,Won,Z04,Decide,20130101,201111,607933.8,1
3,Won,Z05,Deliver,20131001,201202,66.08,1
3,Won,Z04,Decide,20120501,201202,66.08,1
3,Won,Z01,Understand,20120409,201203,65.07,1
3,Won,Z04,Decide,20121231,201204,4645214.7,1
3,Won,Z05,Deliver,20130918,201204,66.52,1
3,Won,Z04,Decide,20130418,201204,2312130.92,1
3,Won,Z05,Deliver,20120504,201205,68.07,1
3,Won,Z05,Deliver,20120504,201205,515994.86,1
3,Won,Z04,Decide,20120504,201205,68.07,1
3,Won,Z04,Decide,20120504,201205,68.07,1
</pre>

Javascript:

var dateFormat = d3.time.format('%Y%m%d');
var monthNameFormat = d3.time.format("%b-%Y");
// Prepare Data
for (var z = 0; z < opportunities.length; z++) {
  opportunities[z].DD = dateFormat.parse(opportunities[z].CompletionDate);
}

opportunities.sort(function(a,b){
  return b.DD - a.DD;
});

var ndx = crossfilter(opportunities);

var dimMonthYear = ndx.dimension(function(d) {
  //return d.CompletionDate
  return monthNameFormat(new Date(d.DD));
});

var phaseUnderstand = dimMonthYear.group().reduceSum(function(d) {
  if (d.Phase === "Z01") {
    return d.Sales;
  } else {
    return 0;
  }
});
var phasePropose = dimMonthYear.group().reduceSum(function(d) {
  if (d.Phase === "Z02") {
    return d.Sales;
  } else {
    return 0;
  }
});
var phaseNegotiate = dimMonthYear.group().reduceSum(function(d) {
  if (d.Phase === "Z03") {
    return d.Sales;
  } else {
    return 0;
  }
});
var phaseDecide = dimMonthYear.group().reduceSum(function(d) {
  if (d.Phase === "Z04") {
    return d.Sales;
  } else {
    return 0;
  }
});
var phaseDeliver = dimMonthYear.group().reduceSum(function(d) {
  if (d.Phase === "Z05") {
    return d.Sales;
  } else {
    return 0;
  }
});

chart
  .width(768)
  .height(480)
  .margins({
    left: 80,
    top: 20,
    right: 10,
    bottom: 80
  })
  .x(d3.scale.ordinal())
  .round(d3.time.month.round)
  .alwaysUseRounding(true)
  .xUnits(dc.units.ordinal)
  .brushOn(false)
  .xAxisLabel('Months')
  .yAxisLabel("Expected Sales (in thousands)")
  .dimension(dimMonthYear)
  .renderHorizontalGridLines(true)
  .renderVerticalGridLines(true)
 // .barPadding(0.1)
  //.outerPadding(0.05)
  .legend(dc.legend().x(130).y(30).itemHeight(13).gap(5))
  .group(phaseUnderstand, "Understand")
  .stack(phasePropose, "Propose")
  .stack(phaseNegotiate, "Negotiate")
  .stack(phaseDecide, "Decide")
  .stack(phaseDeliver, "Deliver")
  .centerBar(true)
  .elasticY(true)
  .elasticX(true)
  .renderlet(function(chart) {
    chart.selectAll("g.x text").attr('dx', '-30').attr(
      'dy', '-7').attr('transform', "rotate(-60)");
  });
chart.render();

Я могу отобразить диаграмму, но не могу отсортировать ось X по месяцам в порядке возрастания и сделать ее упругой вдоль оси X. Если я использую только дату, тогда диаграмма становится слишком детальной (хотя ось X все еще не эластична), что бесполезно для цели, для которой я ее создаю. Вот пример скрипки: https://jsfiddle.net/NewMonk/1hbjwxzy/67/.

1 ответ

Решение

Я настоятельно рекомендую использовать шкалы времени, когда ось X основана на времени. Это просто немного больше работы (и больше риска страшной пустой диаграммы), но это гораздо более гибко и точно.

Прежде всего, нам нужно попросить crossfilter to bin по месяцам. d3.time.month округляет каждую дату до ближайшего месяца:

var dimMonthYear = ndx.dimension(function(d) {
  return d3.time.month(d.DD)
});

Это имеет тот же эффект, что и созданные вами строки, но сохраняет ключи как даты.

Затем мы можем сказать dc.js использовать шкалу времени и asticX:

chart
  .x(d3.time.scale()).elasticX(true)
  .round(d3.time.month.round)
  .alwaysUseRounding(true)
  .xUnits(d3.time.months)

.xUnits это то, что правильно определяет ширину бара (чтобы dc.js мог посчитать количество видимых баров).

Мы можем восстановить monthNameFormat отметьте формат и покажите каждый тик следующим образом:

chart.xAxis()
  .tickFormat(monthNameFormat)
  .ticks(d3.time.months,1);

Наконец, вам может понадобиться удалить пустые корзины при фильтрации на другом графике, чтобы столбцы были удалены, когда они упадут до нуля:

function remove_empty_bins(source_group) {
    return {
        all:function () {
            return source_group.all().filter(function(d) {
                return d.value != 0;
            });
        }
    };
}

Послесловие

@Kush комментирует ниже, что удаление пустых корзин из сложенного набора групп на основе дат не так просто. Это так.

Вот пользовательская версия comb_groups для объединения групп на основе дат:

function combine_date_groups() { // (groups...)
    var groups = Array.prototype.slice.call(arguments);
    return {
        all: function() {
            var alls = groups.map(function(g) { return g.all(); });
            var gm = {};
            alls.forEach(function(a, i) {
                a.forEach(function(b) {
                    var t = b.key.getTime();
                    if(!gm[t]) {
                        gm[t] = new Array(groups.length);
                        for(var j=0; j<groups.length; ++j)
                            gm[t][j] = 0;
                    }
                    gm[t][i] = b.value;
                });
            });
            var ret = [];
            for(var k in gm)
                ret.push({key: new Date(+k), value: gm[k]});
            return ret;
        }
    };
}

Здесь неприятно то, что мы должны преобразовать даты в целые числа, чтобы правильно проиндексировать объекты, а затем преобразовать целые числа обратно в даты при построении данных фиктивной группы.

Примените это так:

var combinedGroup = combine_date_groups(remove_empty_bins(phaseUnderstand), 
    remove_empty_bins(phasePropose), 
    remove_empty_bins(phaseNegotiate), 
    remove_empty_bins(phaseDecide), 
    remove_empty_bins(phaseDeliver));

Вот вспомогательная функция для извлечения стека по индексу:

function sel_stack(i) {
    return function(d) {
    return d.value[i];
  };
}

Теперь мы можем указать стеки так:

chart
  .group(combinedGroup, "Understand", sel_stack(0))
  .stack(combinedGroup, "Propose", sel_stack(1))
  .stack(combinedGroup, "Negotiate", sel_stack(2))
  .stack(combinedGroup, "Decide", sel_stack(3))
  .stack(combinedGroup, "Deliver", sel_stack(4))

Обновленная скрипка здесь: https://jsfiddle.net/gordonwoodhull/8pc0p1we/17/

(Забыл опубликовать скрипку в первый раз, я думаю, что это было примерно в версии 8 до этого приключения.)

Мы настолько далеко за пределами того, для чего изначально были созданы crossfilter и dc.js, это довольно забавно. Данные сложны.

Другие вопросы по тегам