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.
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?
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.