如何用Observable Plot过滤过窄区域的文本标记及优化天气时间线
天气时间线复刻问题(Observable Plot)
我想用Observable Plot复刻天气时间线,包含彩色条带和文本标签。已完成初始版本,但存在以下问题:
- 标签长度常超出对应区域,导致与下一个标签重叠
- 标签未在区域内居中(普通场景可修复,但添加左侧图标后无法解决)
核心问题
- 有没有更优的方式在Observable Plot中创建带标签的彩色区域?当前使用
Plot.areaY实现。 - 如何过滤掉区域过窄、无法容纳的标签?目前仅简单过滤重复天气代码,不清楚如何将时间转换为X轴像素,也不会计算标签自身的像素宽度。
- 希望在文本标签左侧添加小型天气图标(使用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
相关产品推荐
相关产品推荐

