Multi-series bar chart in DC-js
Asked Answered
T

4

8

I'm using DC.js ( lib on top of D3 ) and have a great example of a single series bar chart:

enter image description here

 var xf = crossfilter(data);
 var dim = xf.dimension(function (d) { return d["EmployeeName"]; });
 var group = dim.group().reduceSum(function (d) { return d["AverageSale"]; });

 var chart = dc.barChart(elm);
 chart.barPadding(0.1)
 chart.outerPadding(0.05)
 chart.brushOn(false)
 chart.x(d3.scale.ordinal());
 chart.xUnits(dc.units.ordinal);
 chart.elasticY(true);

 chart.dimension(dim).group(group);
 chart.render();

but I was wondering if it was possible to create a multi dimensional bar chart using this library. For example: Group by Store Name then Group By Employee then y-axis display average sale value ( already calculated by my backend ).

The data looks like:

 [{
    "EmployeeName": "Heather",
    "StoreName" : "Plaza",
    "AverageSaleValue": 200
 }{
    "EmployeeName": "Mellisa",
    "StoreName" : "Plaza",
    "AverageSaleValue": 240
 }, {
    "EmployeeName": "Sarah",
    "StoreName" : "Oak Park",
    "AverageSaleValue": 300
 } ... ]
Terat answered 22/2, 2014 at 22:36 Comment(0)
G
9

If you have a static number of groups to graph, you can achieve the desired effect with a composite chart.

In the example below, I hard coded the gap between the bar charts - I can do this because I know there are 12 months being displayed.

            var actuals = dc.barChart(compositeChart)
                    .gap(65)
                    .group(group)
                    .valueAccessor(function (d) {
                        return d.value.Actual;
                    });

            var budgets = dc.barChart(compositeChart)
                    .gap(65)
                    .group(group)
                    .valueAccessor(function (d) {
                        return d.value.Budget;
                    });

I pass these bar charts to the compose method of a composite chart:

                compositeChart
                    .width(1000)
                    .height(300)
                    .dimension(monthDimension)
                    .group(group)
                    .elasticY(true)
                    .x(d3.time.scale().domain(timeExtent))
                    .xUnits(d3.time.months)
                    .round(d3.time.month.round)
                    .renderHorizontalGridLines(true)
                    .compose([budgets, actuals])
                    .brushOn(true);

Finally, I add a renderlet to move one of the charts to the right a few pixels:

              compositeChart
                    .renderlet(function (chart) {
                        chart.selectAll("g._1").attr("transform", "translate(" + 20 + ", 0)");
                        chart.selectAll("g._0").attr("transform", "translate(" + 1 + ", 0)");
                    });

I know this isn't the cleanest approach but it can work in a pinch.

I hope this helps.

Glaive answered 24/2, 2014 at 14:48 Comment(1)
Thanks, problem is my charts are totally dynamic ... I do have the amount of 'groups' so maybe I could calc the width based on that ... hmm ...Terat
A
2

The closest thing to what you're asking for that comes to mind immediately in dc.js would be a stacked bar chart (example). But I think what you might prefer is a grouped bar chart. I'm not sure that this chart type is currently supported by dc.js. Maybe someone else knows.

Asymptomatic answered 22/2, 2014 at 23:21 Comment(3)
Grouped bar chart is what I was looking for but can't believe dc doesn't have this!Terat
You can achieve that by using transform function of d3Ethbun
@Goutam please explainHydrolysate
I
2

I know I’m late for this but it might help someone else.

To create grouped-bar-chart in dc.js without overwrite the original dc code you can take advantage of ‘pretransition’ event and split the bars to create a group.

I've created an example (jsfiddle)

The magic happens here:

let scaleSubChartBarWidth = chart => {
  let subs = chart.selectAll(".sub");

  if (typeof barPadding === 'undefined') { // first draw only
    // to percentage
    barPadding = BAR_PADDING / subs.size() * 100;
    // each bar gets half the padding
    barPadding = barPadding / 2;
  }

  let startAt, endAt,
      subScale = d3.scale.linear().domain([0, subs.size()]).range([0, 100]);

  subs.each(function (d, i) {

    startAt = subScale(i + 1) - subScale(1);
    endAt = subScale(i + 1);

    startAt += barPadding;
    endAt -= barPadding;

    // polygon(
    //  top-left-vertical top-left-horizontal,
    //  top-right-vertical top-right-horizontal,
    //  bottom-right-vertical bottom-right-horizontal,
    //  bottom-left-vertical bottom-left-horizontal,
    // )

    d3.select(this)
      .selectAll('rect')
      .attr("clip-path", `polygon(${startAt}% 0, ${endAt}% 0, ${endAt}% 100%, ${startAt}% 100%)`);

  });
}; 

...

.on("pretransition", chart => {
    scaleSubChartBarWidth(chart);
})

Complete code:

markup

<div id="chart-container"></div>

Js

//'use strict';
let compositeChart = dc.compositeChart("#chart-container");

const BAR_PADDING = .1; // percentage the padding will take from the bar
const RANGE_BAND_PADDING = .5; // padding between 'groups'
const OUTER_RANGE_BAND_PADDING = 0.5; // padding from each side of the chart

let sizing = chart => {
  chart
    .width(window.innerWidth)
    .height(window.innerHeight)
    .redraw();
};

let resizing = chart => window.onresize = () => sizing(chart);

let barPadding;
let scaleSubChartBarWidth = chart => {
  let subs = chart.selectAll(".sub");

  if (typeof barPadding === 'undefined') { // first draw only
    // to percentage
    barPadding = BAR_PADDING / subs.size() * 100;
    // each bar gets half the padding
    barPadding = barPadding / 2;
  }

  let startAt, endAt,
      subScale = d3.scale.linear().domain([0, subs.size()]).range([0, 100]);

  subs.each(function (d, i) {

    startAt = subScale(i + 1) - subScale(1);
    endAt = subScale(i + 1);

    startAt += barPadding;
    endAt -= barPadding;

    // polygon(
    //  top-left-vertical top-left-horizontal,
    //  top-right-vertical top-right-horizontal,
    //  bottom-right-vertical bottom-right-horizontal,
    //  bottom-left-vertical bottom-left-horizontal,
    // )

    d3.select(this)
      .selectAll('rect')
      .attr("clip-path", `polygon(${startAt}% 0, ${endAt}% 0, ${endAt}% 100%, ${startAt}% 100%)`);

  });
};

let data = [
  {
    key: "First",
    value: [
      {key: 1, value: 0.18},
      {key: 2, value: 0.28},
      {key: 3, value: 0.68}
    ]
  },
  {
    key: "Second",
    value: [
      {key: 1, value: 0.72},
      {key: 2, value: 0.32},
      {key: 3, value: 0.82}
    ]
  },
  {
    key: "Third",
    value: [
      {key: 1, value: 0.3},
      {key: 2, value: 0.22},
      {key: 3, value: 0.7}
    ]
  },
  {
    key: "Fourth",
    value: [
      {key: 1, value: 0.18},
      {key: 2, value: 0.58},
      {key: 3, value: 0.48}
    ]
  }
];

let ndx = crossfilter(data),
    dimension = ndx.dimension(d => d.key),
    group = {all: () => data}; // for simplicity sake (take a look at crossfilter group().reduce())

let barChart1 = dc.barChart(compositeChart)
    .barPadding(0)
    .valueAccessor(d => d.value[0].value)
    .title(d => d.key + `[${d.value[0].key}]: ` + d.value[0].value)
    .colors(['#000']);

let barChart2 = dc.barChart(compositeChart)
    .barPadding(0)
    .valueAccessor(d => d.value[1].value)
    .title(d => d.key + `[${d.value[1].key}]: ` + d.value[1].value)
    .colors(['#57B4F0']);

let barChart3 = dc.barChart(compositeChart)
    .barPadding(0)
    .valueAccessor(d => d.value[2].value)
    .title(d => d.key + `[${d.value[2].key}]: ` + d.value[2].value)
    .colors(['#47a64a']);

compositeChart
  .shareTitle(false)
  .dimension(dimension)
  .group(group)
  ._rangeBandPadding(RANGE_BAND_PADDING)
  ._outerRangeBandPadding(OUTER_RANGE_BAND_PADDING)
  .x(d3.scale.ordinal())
  .y(d3.scale.linear().domain([0, 1]))
  .xUnits(dc.units.ordinal)
  .compose([barChart1, barChart2, barChart3])
  .on("pretransition", chart => {
  scaleSubChartBarWidth(chart)
})
  .on("filtered", (chart, filter) => {
  console.log(chart, filter);
})
  .on("preRedraw", chart => {
  chart.rescale();
})
  .on("preRender", chart => {
  chart.rescale();
})
  .render();

sizing(compositeChart);
resizing(compositeChart);

It's not perfect but it could give a starting point.

Impostume answered 5/7, 2017 at 17:49 Comment(1)
What if I want to create a chart similar to waterfall chart which in this case moves the bars to a specific position in the y-axis.Gummosis
L
0

I was able to do this with a twist of the renderlet technique in the following link renderlet function for coloring

My code goes as follows

.renderlet(function (chart) {
    chart.selectAll('rect.bar').each(function(d){                           
    d3.select(this).attr("fill",                            
        (function(d){
            var colorcode ="grey"
            if(d.x[1] === "Zone1")   
                colorcode ="#ff7373";
            else if(d.x[1] === "Zone2")
                colorcode ="#b0e0e6";
            else if(d.x[1] === "Zone3")
                colorcode ="#c0c0c0";
            else if(d.x[1] === "Zone4")
                colorcode ="#003366";
            else if(d.x[1] === "Zone5")
                colorcode ="#ffa500";
            else if(d.x[1] === "Zone6")
                colorcode ="#468499";
            else if(d.x[1] === "Zone7")
                colorcode ="#660066";   
            return colorcode;
        }))
    }); 

});  

Note : I was using a dimension with 2 values for the series .

Laterality answered 29/4, 2016 at 11:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.