You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Observable Plot过滤过窄区域的文本标记及优化天气时间线

天气时间线复刻问题(Observable Plot)

我想用Observable Plot复刻天气时间线,包含彩色条带和文本标签。已完成初始版本,但存在以下问题:

  • 标签长度常超出对应区域,导致与下一个标签重叠
  • 标签未在区域内居中(普通场景可修复,但添加左侧图标后无法解决)

核心问题

  1. 有没有更优的方式在Observable Plot中创建带标签的彩色区域?当前使用Plot.areaY实现。
  2. 如何过滤掉区域过窄、无法容纳的标签?目前仅简单过滤重复天气代码,不清楚如何将时间转换为X轴像素,也不会计算标签自身的像素宽度。
  3. 希望在文本标签左侧添加小型天气图标(使用Plot的image mark),区域过窄时,若宽度足够则仅显示图标。
    接受基于底层D3 API的解决方案。

当前进度截图

当前进度截图

简易示例

HTML代码

<!DOCTYPE html>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<script src="https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6"></script>

<div id="myplot"></div>

JavaScript代码

const data = [{
    text: '强冻毛毛雨',
    utc: new Date('2024-06-16'), end: new Date('2024-06-17'),
  }, {
    text: '小雪阵',
    utc: new Date('2024-06-17'), end: new Date('2024-06-18'),
  }, {
    text: '晴朗',
    utc: new Date('2024-06-18'), end: new Date('2024-06-22'),
  }, {
    text: '雷暴伴小冰雹',
    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 == '晴朗' ? 'lightgray': 'SkyBlue'
      }),
    Plot.text(data, {
      x: (d) => (Number(d.utc) + Number(d.end))/2,
      text: 'text'
    }),
  ],
});

const div = document.querySelector('#myplot');
div.append(plot);

注:示例中只有“晴朗”的标签应该显示,其余标签均会超出对应区域。


解决方案

1. 更优的彩色区域创建方式

推荐使用Plot.rectY替代Plot.areaY,rectY专门用于绘制矩形区块,更贴合时间分段的场景,代码逻辑更直观,也能避免areaY可能带来的多余填充问题。核心是通过x1(起始时间)、x2(结束时间)定义横向范围,y固定值控制纵向位置,再通过fill设置对应天气的颜色。

2. 过滤窄区域标签

要实现该功能,需要结合D3的时间比例尺和DOM文本测量:

  • 先创建X轴的时间比例尺,将时间值映射为图表像素坐标
  • 计算每个时间区块的像素宽度
  • 通过隐藏DOM元素测量文本的实际渲染宽度
  • 对比区块宽度与文本宽度,过滤掉无法容纳的标签

代码示例:

// 定义图表宽度和时间比例尺
const plotWidth = 800; // 替换为你的实际图表宽度
const xDomain = [d3.min(data, d => d.utc), d3.max(data, d => d.end)];
const xScale = d3.scaleTime().domain(xDomain).range([0, plotWidth]);

// 创建用于测量文本宽度的隐藏元素
const measureEl = document.createElement('span');
measureEl.style.cssText = 'visibility:hidden;position:absolute;white-space:nowrap;font:inherit;';
document.body.appendChild(measureEl);

// 过滤可显示标签的数据
const filteredTextData = data.filter(d => {
  measureEl.textContent = d.text;
  const textWidth = measureEl.offsetWidth;
  const rectWidth = xScale(d.end) - xScale(d.utc);
  return rectWidth > textWidth + 10; // 预留10px边距
});

document.body.removeChild(measureEl);

// 使用filteredTextData绘制文本标签
Plot.text(filteredTextData, {
  x: d => (Number(d.utc) + Number(d.end))/2,
  text: 'text',
  anchor: 'middle'
})

3. 添加图标及窄区域适配逻辑

需要结合image和text标记,根据区块宽度判断显示内容:

  • 准备天气类型与图标URL的映射关系
  • 计算每个区块的宽度,区分三种显示状态:显示图标+文本、仅显示图标、不显示任何内容
  • 分别渲染图标和文本,确保整体在区块内居中

代码示例:

// 天气图标映射(替换为实际图标路径)
const iconMap = {
  '晴朗': 'clear-icon.png',
  '强冻毛毛雨': 'freezing-drizzle-icon.png',
  '小雪阵': 'snow-shower-icon.png',
  '雷暴伴小冰雹': 'thunder-hail-icon.png'
};

const iconSize = 20; // 图标宽高固定值
const measureEl = document.createElement('span');
measureEl.style.cssText = 'visibility:hidden;position:absolute;white-space:nowrap;font:inherit;';
document.body.appendChild(measureEl);

// 预处理数据,标记显示类型
const processedData = data.map(d => {
  const rectWidth = xScale(d.end) - xScale(d.utc);
  measureEl.textContent = d.text;
  const textWidth = measureEl.offsetWidth;
  
  let displayType = 'none';
  if (rectWidth > textWidth + iconSize + 15) { // 足够显示图标+文本
    displayType = 'icon+text';
  } else if (rectWidth > iconSize + 5) { // 仅够显示图标
    displayType = 'icon';
  }
  
  return {...d, displayType, textWidth};
});

document.body.removeChild(measureEl);

// 绘制图标
Plot.image(processedData.filter(d => d.displayType !== 'none'), {
  x: d => {
    const centerX = (xScale(d.utc) + xScale(d.end)) / 2;
    if (d.displayType === 'icon') {
      return centerX; // 图标单独居中
    } else {
      // 图标在文本左侧,整体居中:中心位置减去(文本宽度+图标宽度)/2 + 图标半宽
      return centerX - (d.textWidth + iconSize)/2 + iconSize/2;
    }
  },
  y: 1,
  src: d => iconMap[d.text],
  width: iconSize,
  height: iconSize,
  anchor: 'middle'
}),

// 绘制文本
Plot.text(processedData.filter(d => d.displayType === 'icon+text'), {
  x: d => {
    const centerX = (xScale(d.utc) + xScale(d.end)) / 2;
    // 文本在图标右侧,整体居中:中心位置加上(文本宽度+图标宽度)/2 - 文本半宽
    return centerX + (d.textWidth + iconSize)/2 - d.textWidth/2;
  },
  text: 'text',
  anchor: 'middle'
})

内容的提问来源于stack exchange,提问作者Leftium

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 17:44:59