Как фильтровать текстовый знак Observable Plot, когда «раздел» слишком узкий

Используя Observable Plot, я хочу воссоздать временную шкалу погоды https://merrysky.net с цветными полосами и текстовыми метками.

У меня работает первоначальная версия, но у нее есть некоторые недостатки:

  • Метки часто длиннее, чем «раздел», который они маркируют, поэтому текст перекрывается со следующей меткой.
  • Метки не центрируются в разделе. (Я знаю, как это исправить, за исключением добавления значков слева от меток.)
  • Живая демонстрация
  • Соответствующий исходный код

Вопросы:

  • Прежде всего: есть ли лучший способ создать эти цветные и помеченные разделы в Observable Plot? Мой код использует Plot.areaY
  • Как отфильтровать ярлыки, которые слишком широки для соответствующих разделов? В настоящее время мой код представляет собой простой фильтр, который удаляет повторяющиеся коды погоды. Функция фильтра имеет доступ к элементу данных, но единственная информация, относящаяся к оси X, — это время. Я не уверен, как преобразовать это время в пиксели по оси X. (И не вычисляйте ширину самой метки в пикселях.)
  • Наконец, я хотел бы добавить небольшой значок изображения значка кода погоды слева от текстовой метки. Если раздел слишком узкий, будет отображаться только значок (при достаточной ширине).

Конечно, я открыт для решений, использующих базовый 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>
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
0
1
92
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

По сути, вы спрашиваете, как обнаружить перекрытие. Вот быстрый алгоритм, который я вставил в тот же метод «рендеринга», который мы использовали в вашем последнем вопросе. Я постарался это хорошо прокомментировать:

<!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>

При этом не учитывается ширина раздела по отношению к ширине этикетки. Я добавил к своему вопросу пример фрагмента кода: должна отображаться только метка «Очистить», поскольку другие метки не соответствуют своим разделам. (Это очень плохо, если единственный способ получить ширину текстового знака - это сначала отобразить его... есть ли способ просто обрезать текст, чтобы он не выходил за пределы прямоугольного знака?)

Leftium 12.06.2024 03:22

В идеале слишком длинный текст можно обрезать многоточием, например: «Heavy Freez...»

Leftium 12.06.2024 03:28

@Leftium, см. правки выше. Что касается фразы «Это очень плохо, если единственный способ получить ширину текстового знака», есть способы обойти это. Во-первых, вы можете вынести все это за пределы функции render и просто сделать это как прямой d3 после рисования диаграммы. Я избегал этого, так как мне нравится инкапсуляция этого в пределах вызова сюжета. Во-вторых, вы можете добавить поддельный SVG для визуализации текста и проверки его ширины, которую вы удалите после проверки. Однако оба эти решения кажутся мне менее элегантными, чем тот простой setTimeout, который я использую.

Mark 12.06.2024 16:18

Еще один отличный ответ~ принято! Также ознакомьтесь с решением на основе Observable Plot, которое я придумал: stackoverflow.com/a/78613628/117030

Leftium 12.06.2024 17:32

В дополнение к отличному решению Марка я нашел решение на основе наблюдаемого графика:

  1. Во-первых, используйте Plot.rect вместо Plot.Area. Plot.Area работает, но его интерполяция может привести к странному результату для этого типа использования.
  2. Отобразите специальный массив данных, который объединяет соседние элементы данных с одним и тем же кодом погоды в один элемент данных, поэтому x1 — это время начала, а x2 — время окончания раздела с этим кодом.
  3. Пока мы этим занимаемся, посчитайте xMiddle, чтобы отметить середину секции.
  4. Отправить эти данные в Plot.text() довольно просто.
  5. Невозможно заранее рассчитать, является ли раздел слишком узким, поэтому для свойства text используется функция. Метод plot.scale('x').apply() используется для преобразования единиц времени в пиксельные единицы и помогает определить, не слишком ли узок раздел.
  6. Я просто жестко запрограммировал ширину 80 пикселей (что подходит для моих меток), но любой из методов, предложенных Марком, можно использовать для расчета точной ширины текста.
  7. Существует проблема курицы и яйца, потому что plot = Plot.plot() нужно использовать plot.scale(). Я решил эту проблему, дважды позвонив plot(). Если не вызвать дважды, масштабирование не сработает в первый раз. Таким образом, проверка ширины не работает, и метки не будут скрыты, если секции слишком узкие.

Другие вопросы по теме