dc.js

Adding a horizontal baseline to a SeriesChart in dc.js


So I have a SeriesChart with this code:

'use strict';

var sectorChart = new dc.SeriesChart('#test-chart');

d3.json('php/query.php?option=sectors').then(data => {
    const dateFormatSpecifier = '%Y-%m-%d';
    const dateFormat = d3.timeFormat(dateFormatSpecifier);
    const dateFormatParser = d3.timeParse(dateFormatSpecifier);
    const numberFormat = d3.format('.2f');
    var minDate = new Date();
    var maxDate = new Date();
    var minRatio = .5
    var maxRatio = .5
    data.forEach(d => {
        d.dd = dateFormatParser(d.date);
        d.ratio = +d.ratio; // coerce to number
        if (d.dd < minDate ) minDate = d.dd;
        if (d.dd > maxDate ) maxDate = d.dd;
        if (d.ratio < minRatio ) minRatio = d.ratio
        if (d.ratio > maxRatio ) maxRatio = d.ratio
    });
    
    const ndx = crossfilter(data);
    const all = ndx.groupAll();

    // Dimension is an array of sector and date. 
    // Later we'll take the date for the y-axis 
    // and the sector for the labels
    const dateDimension = ndx.dimension(d => [d.name, d.dd]);
    // group by the average, the value we'll plot
    var avgGroup = dateDimension.group().reduceSum(d => d.average);
    
    sectorChart /* dc.lineChart('#monthly-move-chart', 'chartGroup') */
        .width(990)
        .height(500)
        .chart(c => new dc.LineChart(c))
        .x(d3.scaleTime().domain([minDate, maxDate]))
        .y(d3.scaleLinear().domain([minRatio, maxRatio]))
        .margins({top: 30, right: 10, bottom: 20, left: 40})
        .brushOn(false)
        .yAxisLabel("ratio avg 10")
        .renderHorizontalGridLines(true)
        .dimension(dateDimension)
        .group(avgGroup)
        .seriesAccessor(d => d.key[0])
        .keyAccessor(d => d.key[1])
        .valueAccessor(d => +d.value)
        .legend(dc.legend().x(450).y(15).itemHeight(13).gap(5).horizontal(1).legendWidth(340).autoItemWidth(true));        
    dc.renderAll();
});

It works all right, but now I would like to add a black horizontal line at 0.5 value. I guess I could modify the query to return a fictious "name" to the dataset with all values at 0.5, but that won't allow me to control the color, and anyway I would like to know if there is a better way, not to mess with the returned data.

Edit: According to Gordon's info, I have transposed as good as I could the vertical line to a horizontal line. It has worked, with some eccentricities. One, the line starts at the left side of the viewport, not on the y axis as it should. Two, and more mysterious, the line is drawn at 0.51 instead of 0.50. I have created a fiddle if anybody wants to play with it.

Fiddle

Based on Gordon's solution I have written a function, with the idea not to burden the chart's definition with all that code. Not fully happy with it, particularly the use of random, but anyways, the world keeps turning and other things to do. In any case, I leave it here in case somebody find it useful.

/**
    * Draws a fixed line horizontally or vertically in a dc.js chart
    * To be used  in the "pretransition" event
    * @example
    * .on('pretransition', function (chart) {
    *   dcUtilFixedLine(chart, true, 0.5, [{attr: 'stroke', value: 'red'}] )
    *  }
    * @param (Chart} [chart] The chart provided by the triggered event
    * @param (Boolean) [horizontal] True if the line is horizontal, false if vertical
    * @param (Object) [value] Value of the point where the line is to be inserted,
    *                   usually the type of the dimension if vertical or group if horizontal
    * @param (Array) [attributes]  Array of Objects with attr/value pairs)
    *         giving the  attributes added to the line. 
    *         It defaults to stroke = black, stroke-width = 1.     
    */
 function dcUtilFixedLine(chart, horizontal, value, attributes ) {
    if (horizontal) {
        var extra_data = [
           {y: chart.y()(value) + chart.margins().top, x: chart.margins().left},
           {y: chart.y()(value) + chart.margins().top, x: chart.margins().left + chart.effectiveWidth()}
       ]
    } else {
        var extra_data = [
            {x: chart.x()(value) + chart.margins().left, y: chart.margins().top},
            {x: chart.x()(value) + chart.margins().left, y: chart.margins().top + chart.effectiveHeight()}
        ]
    }
    var addedPath = 'extraLine'  + Math.random().toString().substr(2, 8)
    var line = d3.line().x(d => d.x ).y(d => d.y );
    var chartBody = chart.select('g');
    var path = chartBody.selectAll('path.'+addedPath).data([extra_data]);
            
    path = path.enter()
            .append('path')
            .attr('class', addedPath)
            .attr('id', 'oeLine')
            .attr('stroke', 'black')
            .merge(path);
    
    attributes.forEach( attribute => path.attr(attribute.attr, attribute.value))
            
    path.attr('d', line);
 }

And forked and added it to the new fiddle


Solution

  • Although you can add artificial data series in order to display extra lines, it's often easier to "escape to D3", especially if the lines are static and don't move.

    The row vertical line example shows how to do this.

    The basic outline is:

    1. use the pretransition event to draw something each time the chart is updated
    2. create two x/y points of data to draw, using the chart's scales and margins to determine pixel coordinates
    3. select a path with a unique class name (here .extra) and join the data to it

    For a horizontal line, it could look like this:

        .on('pretransition', function(chart) {
            var y_horiz = 0.5;
            var extra_data = [
                {y: chart.y()(y_horiz) + chart.margins().top, x: chart.margins().left},
                {y: chart.y()(y_horiz) + chart.margins().top, x: chart.margins().left + chart.effectiveWidth()}
            ];
            var line = d3.line()
                .x(function(d) { return d.x; })
                .y(function(d) { return d.y; });
            var chartBody = chart.select('g');
            var path = chartBody.selectAll('path.extra').data([extra_data]);
            path = path.enter()
                    .append('path')
                    .attr('class', 'extra')
                    .attr('stroke', 'black')
                    .attr('id', 'oeLine')
                    .attr("stroke-width", 1)
                    .merge(path);
            path.attr('d', line);
        });
    

    Each X coordinate is offset by chart.margins().left, each Y coordinate by chart.margins().top.

    The width of the chart in pixels is chart.effectiveWidth() and the height in pixels is chart.effectiveHeight(). (These are just the .width() and .height() with margins subtracted.)

    screenshot of horizontal line at 0.50

    Fork of your fiddle

    (Note: the example apparently has a typo, using oeExtra instead of extra in the join. This could cause multiple lines to be drawn when the chart is updated. The class in the select should match the class in the join.)