Search code examples
dc.jscrossfilter

dc.js - display filter values as labels on brush


Could anyone tell me how to customize the brush to display filter values as labels?

I would like to get the same style as the one marked with arrows in the following screenshot but I don't know how to get it, and I haven't seen any example.

Custom Brush filter example


Solution

  • This is a great question, and I am surprised no one has asked this before. Obviously, dc.js can display the filter values in the text above the chart, but putting it right on the brush is really cool!

    Any dc.js chart will allow you to listen for the pretransition event and draw your own annotations using D3.

    Let's do that:

      chart.on('pretransition', function(chart) {
        let brushBegin = [], brushEnd = []; // 1
        if(chart.filter()) {
          brushBegin = [chart.filter()[0]]; // 2
          brushEnd = [chart.filter()[1]];
        }
        let beginLabel = chart.select('g.brush') // 3
          .selectAll('text.brush-begin')
          .data(brushBegin); // 4
        beginLabel.exit().remove(); // 5
        beginLabel = beginLabel.enter()
          .append('text') // 6
          .attr('class', 'brush-begin') // 7
          .attr('text-anchor', 'end')
          .attr('dominant-baseline', 'text-top')
          .attr('fill', 'black')
          .attr('y', chart.margins().top)
          .attr('dy', 4)
          .merge(beginLabel); // 8
        beginLabel
          .attr('x', d => chart.x()(d))
          .text(d => d.toFixed(2)); // 9
        let endLabel = chart.select('g.brush')
          .selectAll('text.brush-end')
          .data(brushEnd);
        endLabel.exit().remove();
        endLabel = endLabel.enter()
          .append('text')
          .attr('class', 'brush-end')
          .attr('text-anchor', 'begin')
          .attr('dominant-baseline', 'text-top')
          .attr('fill', 'black')
          .attr('y', chart.margins().top)
          .attr('dy', 4)
          .merge(endLabel);
        endLabel
          .attr('x', d => chart.x()(d))
          .text(d => d.toFixed(2));
      })
    

    This looks like a lot of code; it's really doing the same thing twice, once for each label. Let's look at how the first label is shown.

    1. D3 binds array data to elements. We will bind labels at the beginning and end of the brush each to an array of zero (brush hidden) or one element (brush shown). This line defaults the arrays to empty.
    2. If there is a filter active, we will set the arrays to one-element arrays containing the begin and end values of the brush.
    3. Standard D3 boilerplate: select the parent element (d.brush), then select the elements we want to create, update, or destroy, then
    4. Bind the zero- or one-element arrays to the selections
    5. If the brush was just hidden, delete the label
    6. If the brush was just shown, add the label
    7. and initialize it with the needed SVG attributes and the class brush-begin which we just used in binding. Most of these attributes are to get the label position correct.
    8. Merge the selections together, so now we have an insert+modify selection
    9. Apply the X position attribute and change the text change whenever the brush changes.

    screenshot of brush with begin/end labels

    https://jsfiddle.net/gordonwoodhull/w4xhv8na/33/

    Getting white-on-black labels is actually not trivial but I hope to return to that later.