javascriptobservable-plot

Multiple independent line marks


Using Observable Plot v0.6, I'd like to add more than one line mark with independent data to my plot and have the legend generated automatically. Using d3.js v7 to change the HTML body after running b = d3.select('body'):

b.html('').append('li').node().append(Plot.plot({
color: {legend:true},
marks: [
Plot.lineY({length:6, title:"CH1"}, {y:[1,4,2,5,3,6],x:[1,2,3,4,5,6]}),
Plot.lineY({length:4, title:"CH2"}, {y:[2,4,1,5],x:[1,2.5,4.5,6]}),
]}))

But that produces neither a legend nor different colours.

I can directly control the colours like so:

b.html('').append('li').node().append(Plot.plot({
color: {legend:true},
marks: [
Plot.lineY({length:6}, {y:[1,4,2,5,3,6],x:[1,2,3,4,5,6], stroke:"red"}),
Plot.lineY({length:4}, {y:[2,4,1,5],x:[1,2.5,4.5,6], stroke:"blue"}),
]}))

but there still is no legend.

Most examples I found assume all lines to use the same x points, but this is not true in my case. Also for my application I'd like to have 1000s of points, so a compact representation (like in the example above) would probably preferable over something like

{
  {x:1, y:1, ch:1},
  {x:2, y:4, ch:1},
  {x:3, y:2, ch:1},
  {x:4, y:5, ch:1},
  {x:5, y:3, ch:1},
  {x:6, y:6, ch:1},
  {x:1, y:2, ch:2},
  {x:2.5, y:4, ch:2},
  {x:4.5, y:1, ch:2},
  {x:6, y:5, ch:2}
}

but I'm open to re-arranging my data if really necessary.

Update 1

I found out that I can easily generate a legend manually, e.g.

b.html('').append('li').node().append(
  Plot.legend({color: {type:"categorical", domain:"ABC"}})
)

and I would probably just have to populate its color.domain parameter with all the marks' titles/categories, apart from syncing the colours somehow between them.

Unfortunately, I haven't yet found a way to add that legend to the overall Plot.plot call, e.g. when I try

b.html('').append('li').node().append(Plot.plot({
color: {legend:true},
marks: [
  Plot.lineY({length:6, title:"CH1"}, {y:[1,4,2,5,3,6],x:[1,2,3,4,5,6]}),
  Plot.lineY({length:4, title:"CH2"}, {y:[2,4,1,5],x:[1,2.5,4.5,6]}),
  Plot.legend({color: {type:"categorical", domain:"ABC"}}),
]}))

I get a TypeError: invalid mark; missing render function, i.e. the legend is not a mark.

The Spike map example on Observable does create a custom legend (see function legendSpike), but it seems I'd have to use regular marks instead of legend to draw, which would be more cumbersome.

Any ideas?


Solution

  • I couldn't find a way to generate the legend within the svg element generated by Plot.plot, but as a workaround I grouped them in a div.

    To make the colour scales match between the legend and the plot, I created the Plot.scale object upfront so I could use it in the generation of both.

    Here is the complete code:

    b = d3.select('body');
    
    data = [  // Array of independent data channels
        {label: "CH1", y:[1,4,2,5,3,6], x:[1,2,3,4,5,6]},
        {label: "CH2", y:[2,4,1,5], x:[1,2.5,4.5,6]},
    ]
    
    color_scale = Plot.scale({color: {  // create colour scale object
        type: "categorical", 
        domain: data.map(channel=>channel.label)  // assign a colour to each channel
    }});
    
    marks = data.map(function(channel) {  // create a lineY plot for each channel
        return Plot.lineY(
            {length: channel.x.length},  // calculate the data length
            {
                stroke: color_scale.apply(channel.label),  // set stroke colour according to each channel's label
                x: channel.x, y: channel.y  // assign x and y from each channel
            } 
        );
    })
    
    b.html('').append('div')  // replace HTML body with a new div to group plots and legend
        .call(  // create legend
            e=>e.node().append(Plot.legend({color: color_scale}))
        )
        .call(  // create channel plots
            e=>e.node().append(
                Plot.plot({
                    marks: marks
                })
            )
        );
    

    If someone can provide similar code with the legend generated inside the Plot.plot SVG (and preferably without the manual management of color_scale), I'll happily accept another answer.