Using Observable Plot, I wish to recreate the https://merrysky.net weather timeline with colored bands and text labels.
I have gotten an initial version working, but it has some flaws:
- The labels are often longer than the "section" they are labeling, so the text overlaps with the next label.
- The labels are not centered on the section. (I know how to fix this, except when adding icons to the left of the labels.)
- Live demo
- Relevant source code
Questions:
- First of all: is there a better way to create these colored & labeled sections in Observable Plot? My code uses Plot.areaY
- How do I filter out the labels that are too wide for their sections? Currently my code is a simple filter that removes repeated weather codes. The filter function has access to the data item, but the only information related to the X axis is time. I am not sure how to convert this time into pixels on the X axis. (Nor calculate the width of the label itself in pixels.)
- Finally, I would like to add a small image mark of the weather code icon to the left of the text label. When the section is too narrow, only the icon would display (if there is enough width.)
Of course, I am open to solutions using the underlying D3 API, as well.
Screenshot of current progress:
UPDATE: added simple example:
const data = [{
text: 'Heavy Freezing Drizzle',
utc: new Date('2024-06-16'), end: new Date('2024-06-17'),
}, {
text: 'Light Snow Showers',
utc: new Date('2024-06-17'), end: new Date('2024-06-18'),
}, {
text: 'Clear',
utc: new Date('2024-06-18'), end: new Date('2024-06-22'),
}, {
text: 'Thunderstorms with light hail',
utc: new Date('2024-06-22'), end: new Date('2024-06-23'),
}]
const plot = Plot.plot({
height: 100,
marks: [
Plot.rectY(data, {
x1: (d) => d.utc,
x2: (d) => d.end,
y: 1,
fill: (d) => d.text == 'Clear' ? 'lightgray': 'SkyBlue'
}),
Plot.text(data, {
x: (d) => (Number(d.utc) + Number(d.end))/2,
text: 'text'
}),
],
});
const div = document.querySelector('#myplot');
div.append(plot);
<!DOCTYPE html>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/[email protected]"></script>
"Clear" is the only text mark that should be rendered because the other text marks overflow their section rect marks.
<div id="myplot"></div>
2
Answers
In addition to Mark's excellent solution, I found an Observable Plot-based solution:
Plot.text
Plot.rect
instead ofPlot.Area
.Plot.Area
works, but its interpolation can cause strange output for this type of use case.x1
is the starting time andx2
is the ending time for a section with that code.xMiddle
to mark the middle of the section.Plot.text()
is pretty straight-forward.text
property. Methodplot.scale('x').apply()
is used to convert time units to pixel units and helps determine if the section is too narrow.plot = Plot.plot()
needs to useplot.scale()
. I hacked around this problem by callingplot()
twice. If not called twice, the scaling doesn't work the first time. Thus the width check doesn't work and labels won't be hidden when the sections are too narrow.You are essentially asking how to do overlap detection. Here’s a quick algorithm that I stuffed into the same style "render" method we used in your last question. I’ve tried to comment it well:
Edits Based on Comments
Here’s a modification that "ellipses" the text based on the size of the sibling
rect
s.