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

Vega图表实现Brush区间对齐至周周期的需求

解决方案:调整Vega Overview+Detail图表的X轴单位与Brush对齐规则

以下是修改后的完整Vega配置,实现两个核心需求:X轴最小单位为天,Brush选中区间自动对齐周的整数倍且至少显示7天:

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "description": "Area charts of stock prices, with an interactive overview and filtered detail views.",
  "width": 720,
  "height": 480,
  "padding": 5,
  "data": [
    {
      "name": "sp500",
      "url": "data/sp500.csv",
      "format": {"type": "csv", "parse": {"price": "number", "date": "date"}},
      "transform": [{"type": "filter", "expr": "datum.date < 957103200000"}]
    }
  ],
  "signals": [{"name": "detailDomain"}],
  "marks": [
    {
      "type": "group",
      "name": "detail",
      "encode": {"enter": {"width": {"value": 720}, "height": {"value": 390}}},
      "scales": [
        {
          "name": "xDetail",
          "type": "time",
          "range": "width",
          "domain": {"data": "sp500", "field": "date"},
          "domainRaw": {"signal": "detailDomain"}
        },
        {
          "name": "yDetail",
          "type": "linear",
          "range": [390, 0],
          "domain": {"data": "sp500", "field": "price"},
          "nice": true,
          "zero": true
        }
      ],
      "axes": [
        {
          "orient": "bottom",
          "scale": "xDetail",
          "format": "%Y-%m-%d",
          "tickMinStep": 86400000
        },
        {"orient": "left", "scale": "yDetail"}
      ],
      "marks": [
        {
          "type": "group",
          "encode": {
            "enter": {
              "height": {"field": {"group": "height"}},
              "width": {"field": {"group": "width"}},
              "clip": {"value": true}
            }
          },
          "marks": [
            {
              "type": "area",
              "from": {"data": "sp500"},
              "encode": {
                "update": {
                  "x": {"scale": "xDetail", "field": "date"},
                  "y": {"scale": "yDetail", "field": "price"},
                  "y2": {"scale": "yDetail", "value": 0},
                  "fill": {"value": "steelblue"}
                }
              }
            }
          ]
        }
      ]
    },
    {
      "type": "group",
      "name": "overview",
      "encode": {
        "enter": {
          "x": {"value": 0},
          "y": {"value": 430},
          "height": {"value": 70},
          "width": {"value": 720},
          "fill": {"value": "transparent"}
        }
      },
      "signals": [
        {
          "name": "brush",
          "value": 0,
          "on": [
            {"events": "@overview:pointerdown", "update": "[x(), x()]"},
            {
              "events": "[@overview:pointerdown, window:pointerup] > window:pointermove!",
              "update": "[brush[0], clamp(x(), 0, width)]"
            },
            {
              "events": {"signal": "delta"},
              "update": "clampRange([anchor[0] + delta, anchor[1] + delta], 0, width)"
            }
          ]
        },
        {
          "name": "anchor",
          "value": null,
          "on": [{"events": "@brush:pointerdown", "update": "slice(brush)"}]
        },
        {
          "name": "xdown",
          "value": 0,
          "on": [{"events": "@brush:pointerdown", "update": "x()"}]
        },
        {
          "name": "delta",
          "value": 0,
          "on": [
            {
              "events": "[@brush:pointerdown, window:pointerup] > window:pointermove!",
              "update": "x() - xdown"
            }
          ]
        },
        {
          "name": "detailDomain",
          "push": "outer",
          "on": [
            {
              "events": {"signal": "brush"},
              "update": "span(brush) ? alignToWeekRange(invert('xOverview', brush)) : null"
            }
          ]
        }
      ],
      "scales": [
        {
          "name": "xOverview",
          "type": "time",
          "range": "width",
          "domain": {"data": "sp500", "field": "date"}
        },
        {
          "name": "yOverview",
          "type": "linear",
          "range": [70, 0],
          "domain": {"data": "sp500", "field": "price"},
          "nice": true,
          "zero": true
        }
      ],
      "axes": [{"orient": "bottom", "scale": "xOverview"}],
      "marks": [
        {
          "type": "area",
          "interactive": false,
          "from": {"data": "sp500"},
          "encode": {
            "update": {
              "x": {"scale": "xOverview", "field": "date"},
              "y": {"scale": "yOverview", "field": "price"},
              "y2": {"scale": "yOverview", "value": 0},
              "fill": {"value": "steelblue"}
            }
          }
        },
        {
          "type": "rect",
          "name": "brush",
          "encode": {
            "enter": {
              "y": {"value": 0},
              "height": {"value": 70},
              "fill": {"value": "#333"},
              "fillOpacity": {"value": 0.2}
            },
            "update": {
              "x": {"signal": "brush[0]"},
              "x2": {"signal": "brush[1]"}
            }
          }
        },
        {
          "type": "rect",
          "interactive": false,
          "encode": {
            "enter": {
              "y": {"value": 0},
              "height": {"value": 70},
              "width": {"value": 1},
              "fill": {"value": "firebrick"}
            },
            "update": {"x": {"signal": "brush[0]"}}
          }
        },
        {
          "type": "rect",
          "interactive": false,
          "encode": {
            "enter": {
              "y": {"value": 0},
              "height": {"value": 70},
              "width": {"value": 1},
              "fill": {"value": "firebrick"}
            },
            "update": {"x": {"signal": "brush[1]"}}
          }
        }
      ]
    }
  ],
  "functions": [
    {
      "name": "alignToWeekRange",
      "parameters": ["rawRange"],
      "body": [
        "const dayMs = 86400000;",
        "const weekMs = 7 * dayMs;",
        "let [start, end] = rawRange;",
        "// 对齐起始日期到最近的周日(周日为一周起始,可根据需求调整为周一)",
        "const startDay = Math.floor(start / dayMs) * dayMs;",
        "const startWeekOffset = (new Date(startDay)).getDay() * dayMs;",
        "start = startDay - startWeekOffset;",
        "// 计算跨度,确保至少7天,且为7的倍数",
        "let span = end - start;",
        "const minSpan = weekMs;",
        "if (span < minSpan) {",
        "  span = minSpan;",
        "  end = start + span;",
        "} else {",
        "  // 向上取整到最近的7天倍数",
        "  span = Math.ceil(span / weekMs) * weekMs;",
        "  end = start + span;",
        "}",
        "// 确保不超过数据的最大日期",
        "const maxDate = data('sp500')[data('sp500').length - 1].date;",
        "if (end > maxDate) {",
        "  end = maxDate;",
        "  start = end - Math.max(minSpan, Math.floor((end - start) / weekMs) * weekMs);",
        "}",
        "return [start, end];"
      ]
    }
  ]
}

关键修改说明

1. 强制X轴最小单位为天

在detail区域的X轴配置中添加两个属性:

  • "format": "%Y-%m-%d":指定日期显示格式为年-月-日,避免显示小时、分钟等更小单位
  • "tickMinStep": 86400000:设置刻度最小间隔为1天(86400000毫秒),确保刻度只会按天生成

2. Brush区间对齐周的整数倍并限制最小跨度

  • 添加自定义函数alignToWeekRange:负责将brush选中的原始日期区间处理为对齐周起始、跨度为7天倍数的区间
  • 修改detailDomain的更新逻辑:调用alignToWeekRange(invert('xOverview', brush)),将原始brush区间转换为符合要求的对齐后区间
  • 函数逻辑说明:
    • 将起始日期对齐到最近的周日(可通过修改getDay()的判断调整为周一作为周起始)
    • 确保区间跨度至少为7天,不足时自动扩展为7天
    • 将超过7天的跨度向上取整为最近的7天倍数(如8天→14天)
    • 处理边界情况,避免区间超出数据的最大日期

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 13:48:10