Dc.js applying range Chart to multiple graphs
Asked Answered
L

2

7

I have been working with dc.js for the last couple of weeks but there is one problem I just don't seem to be able to figure out.

I want to be able to change the scales of four different charts based on a single chart with a brush filter. Something along the lines of:

priorityTotChart
  .width(2*w/3).height(h/3)
  .margins({top: 10, right: 50, bottom: 20, left: 40})
  .dimension(dateDim)
  .group(priorityTotal)
  .centerBar(true)
  .gap(1)
  .x(d3.time.scale().domain([minDate,maxDate]))
  .elasticY(true)
  .brushOn(true);

var translate = 3;  

priority1Chart.width(w/3)
  .height(h/6)
  .transitionDuration(300)
  .margins({top: 10, right: 50, bottom: 25, left: 40})
  .dimension(dateDim)
  .group(priority1)
  .mouseZoomable(false)
  .x(d3.time.scale().domain([minDate,maxDate]))
  .xUnits(d3.time.months)
  .elasticY(true)
  .brushOn(false)
  .ordinalColors(["red"])
  .rangeChart(priorityTotChart)
  .yAxisLabel("Hits per day")
  .renderArea(true)
  .renderlet(function (chart) {
    chart.selectAll("g._1").attr("transform", "translate(" + translate + ", 0)");
});

priority2Chart.width(w/3)
  .height(h/6)
  .transitionDuration(300)
  .margins({top: 10, right: 50, bottom: 25, left: 40})
  .dimension(dateDim)
  .group(priority2)
  .mouseZoomable(false)
  .x(d3.time.scale().domain([minDate,maxDate]))
  .xUnits(d3.time.months)
  .elasticY(true)
  .brushOn(false)
  .ordinalColors(["orange"])
  .rangeChart(priorityTotChart)
  .yAxisLabel("Hits per day")
  .renderArea(true)
  .renderlet(function (chart) {
    chart.selectAll("g._1").attr("transform", "translate(" + translate + ", 0)");
});

priority3Chart.width(w/3)
  .height(h/6)
  .transitionDuration(300)
  .margins({top: 10, right: 50, bottom: 25, left: 40})
  .dimension(dateDim)
  .group(priority3)
  .mouseZoomable(false)
  .x(d3.time.scale().domain([minDate,maxDate]))
  .xUnits(d3.time.months)
  .elasticY(true)
  .brushOn(false)
  .ordinalColors(["blue"])
  .rangeChart(priorityTotChart)
  .yAxisLabel("Hits per day")
  .renderArea(true)
  .renderlet(function (chart) {
    chart.selectAll("g._1").attr("transform", "translate(" + translate + ", 0)");
});

priority4Chart.width(w/3)
  .height(h/6)
  .transitionDuration(300)
  .margins({top: 10, right: 50, bottom: 25, left: 40})
  .dimension(dateDim)
  .group(priority4)
  .mouseZoomable(false)
  .x(d3.time.scale().domain([minDate,maxDate]))
  .xUnits(d3.time.months)
  .elasticY(true)
  .brushOn(false)
  .ordinalColors(["green"])
  .rangeChart(priorityTotChart)
  .yAxisLabel("Hits per day")
  .renderArea(true)
  .renderlet(function (chart) {
    chart.selectAll("g._1").attr("transform", "translate(" + translate + ", 0)");
});

However this method doesn't seem to work as only the last chart that uses .rangeChart(priorityTotChart) will be updated.

I can get around this problem by making each graph depend on the range of the previous graph i.e. priority1Chart has .rangeChart(priorityTotChart), and priority2Chart has .rangeChart(priority1Chart) etc., but this method is very slow. So I was hoping there was a better way to achieve the same effect?

Thanks in advance!

Lorettelorgnette answered 12/12, 2014 at 13:58 Comment(0)
L
5

Another way of asking your question is, "how can I attach multiple focus charts to a single range chart?"

(At least, if you only care about brushing the range chart zooming the focus charts, not the other way around.)

Given this new question, take a look at the implementation of coordinateGridMixin.focusChart:

_chart.focusChart = function (c) {
    if (!arguments.length) {
        return _focusChart;
    }
    _focusChart = c;
    _chart.on('filtered', function (chart) {
        if (!chart.filter()) {
            dc.events.trigger(function () {
                _focusChart.x().domain(_focusChart.xOriginalDomain());
            });
        } else if (!rangesEqual(chart.filter(), _focusChart.filter())) {
            dc.events.trigger(function () {
                _focusChart.focus(chart.filter());
            });
        }
    });
    return _chart;
};

All we need to do here is make a new version of the function that takes multiple focus charts. Since this function doesn't access any internal data, we can "monkey patch" any coordinate grid chart to add the functionality:

chart1.focusCharts = function (chartlist) {
    if (!arguments.length) {
        return this._focusCharts;
    }
    this._focusCharts = chartlist; // only needed to support the getter above
    this.on('filtered', function (range_chart) {
        if (!range_chart.filter()) {
            dc.events.trigger(function () {
                chartlist.forEach(function(focus_chart) {
                    focus_chart.x().domain(focus_chart.xOriginalDomain());
                });
            });
        } else chartlist.forEach(function(focus_chart) {
            if (!rangesEqual(range_chart.filter(), focus_chart.filter())) {
                dc.events.trigger(function () {
                    focus_chart.focus(range_chart.filter());
                });
            }
        });
    });
    return this;
};

We also need to copy the rangesEqual helper function out of coordinateGridMixin in order to get this working.

I created an example and added it to the dc.js examples:

http://dc-js.github.io/dc.js/examples/multi-focus.html

You can see the source here:

https://github.com/dc-js/dc.js/blob/master/web/examples/multi-focus.html

This would be nice to support natively, including the opposite case zoom -> filter, which does require changing dc.js internals. I added an issue to track the idea: https://github.com/dc-js/dc.js/issues/820

Lanta answered 31/12, 2014 at 19:58 Comment(0)
B
3

The range chart works on the .x()-scale of the focus chart.

What you can do is to define one scale globally and use it for every chart that should be controlled by the range chart. Only one of the focus charts has to set the .rangeChart() property.

That is undefined behaviour I think which means this could change in the future.

Pseudo code:

var xScale = d3.scale.linear();
var xScaleRange = d3.scale.linear();

var rangeChart = dc.lineChart('#R').x(xScaleRange).[...];

var focusChartA = dc.barChart('#A').x(xScale).rangeChart(rangeChart).[...];
var focusChartB = dc.barChart('#B').x(xScale).[...];
Buckwheat answered 4/3, 2015 at 8:54 Comment(2)
This is an interesting solution. Not quite as safe as my solution below but it should work in most cases.Lanta
great solution! Even after many years from your answer you have been very useful for my work. thank you!Depositor

© 2022 - 2024 — McMap. All rights reserved.