Используя Observable Plot, я хочу воссоздать временную шкалу погоды https://merrysky.net с цветными полосами и текстовыми метками.
У меня работает первоначальная версия, но у нее есть некоторые недостатки:
Вопросы:
Конечно, я открыт для решений, использующих базовый API D3.
Скриншот текущего прогресса:
ОБНОВЛЕНИЕ: добавлен простой пример:
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>


![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


По сути, вы спрашиваете, как обнаружить перекрытие. Вот быстрый алгоритм, который я вставил в тот же метод «рендеринга», который мы использовали в вашем последнем вопросе. Я постарался это хорошо прокомментировать:
<!DOCTYPE html>
<div id = "myplot"></div>
<script src = "https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src = "https://cdn.jsdelivr.net/npm/@observablehq/[email protected]"></script>
<script type = "module">
const div = document.querySelector('#myplot');
const codes = ['tiny', 'very, very, very, very, very, large'];
const data = [];
for (let i = 0; i < 20; i++) {
data.push({
x: i,
y: Math.random() * 100,
code: codes[Math.floor(Math.random() * codes.length)] //<-- random label of varying size
});
}
const plot = Plot.plot({
marks: [
Plot.text(data, {
x: 'x',
y: 100,
textAnchor: 'start',
text: function (d) {
return d.code;
},
render: (i, s, v, d, c, next) => {
const g = next(i, s, v, d, c),
textLabels = d3.select(g).selectAll('text').nodes();
setTimeout(() => { //<-- run in setTimeout as we need the text elements to render to get their size
for (let i = 0; i < textLabels.length - 1; i++) { //<-- loop the labels
const cu = textLabels[i],
cuBBox = cu.getBoundingClientRect(); //<-- get the size of the current label
for (let j = i + 1; j < textLabels.length; j++) { //<-- loop the next N adjacent labels
const ne = textLabels[j],
neBBox = ne.getBoundingClientRect();
if (cuBBox.x + cuBBox.width > neBBox.x) {
d3.select(ne).remove(); //<-- if the adjacent label overlaps current, remove adjacent
} else {
i = j - 1; //<-- we've found the first adjacent that doesn't overlap, return to top loop and let this one become our next "current"
break;
}
}
}
},0);
return g;
},
}),
Plot.line(data, { x: 'x', y: 'y' }),
],
});
div.append(plot);
</script>Правки на основе комментариев
Вот модификация, которая «эллипсирует» текст в зависимости от размера родственных rects.
<!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>
<script>
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',
render: (i, s, v, d, c, next) => {
const g = next(i, s, v, d, c);
setTimeout(() => {
const textLabels = d3.select(g).selectAll('text').nodes(),
rects = d3.select(g.previousSibling).selectAll('rect').nodes();
for (let i = 0; i < textLabels.length; i++) {
const t = textLabels[i],
r = rects[i],
rW = r.getBBox().width;
let txt = v.text[i],
tW = t.getComputedTextLength();
while (tW > rW && txt.length > 0) {
txt = txt.slice(0, -1);
t.textContent = txt + "...";
tW = t.getComputedTextLength();
}
}
}, 10);
return g;
},
}),
],
});
const div = document.querySelector('#myplot');
div.append(plot);
</script>При этом не учитывается ширина раздела по отношению к ширине этикетки. Я добавил к своему вопросу пример фрагмента кода: должна отображаться только метка «Очистить», поскольку другие метки не соответствуют своим разделам. (Это очень плохо, если единственный способ получить ширину текстового знака - это сначала отобразить его... есть ли способ просто обрезать текст, чтобы он не выходил за пределы прямоугольного знака?)
В идеале слишком длинный текст можно обрезать многоточием, например: «Heavy Freez...»
@Leftium, см. правки выше. Что касается фразы «Это очень плохо, если единственный способ получить ширину текстового знака», есть способы обойти это. Во-первых, вы можете вынести все это за пределы функции render и просто сделать это как прямой d3 после рисования диаграммы. Я избегал этого, так как мне нравится инкапсуляция этого в пределах вызова сюжета. Во-вторых, вы можете добавить поддельный SVG для визуализации текста и проверки его ширины, которую вы удалите после проверки. Однако оба эти решения кажутся мне менее элегантными, чем тот простой setTimeout, который я использую.
Еще один отличный ответ~ принято! Также ознакомьтесь с решением на основе Observable Plot, которое я придумал: stackoverflow.com/a/78613628/117030
В дополнение к отличному решению Марка я нашел решение на основе наблюдаемого графика:
Plot.Area. Plot.Area работает, но его интерполяция может привести к странному результату для этого типа использования.x1 — это время начала, а x2 — время окончания раздела с этим кодом.xMiddle, чтобы отметить середину секции.Plot.text() довольно просто.text используется функция. Метод plot.scale('x').apply() используется для преобразования единиц времени в пиксельные единицы и помогает определить, не слишком ли узок раздел.plot = Plot.plot() нужно использовать plot.scale(). Я решил эту проблему, дважды позвонив plot(). Если не вызвать дважды, масштабирование не сработает в первый раз. Таким образом, проверка ширины не работает, и метки не будут скрыты, если секции слишком узкие.