如何实现Vega中顶部图表Brush随底部Brush平移同步?
实现顶部Brush随Detail区域同步平移
我有两个垂直排列的面积图(改编自Vega官方示例)。底部(overview)区域的Brush可缩放顶部(detail)区域,顶部区域新增了一个Brush,当前已选中顶部图表的4-7月区间。

当用底部Brush平移顶部区域时(下图红色箭头所示),顶部Brush固定不动,而顶部区域随平移变化。预期效果是顶部Brush随顶部区域同步平移,始终覆盖4-7月区间(即移至虚线标记区域)。

如何实现顶部Brush随顶部区域同步平移?
解决方案
核心思路是记录顶部Brush的时间区间而非像素位置,当detailDomain变化时,基于当前比例尺重新计算Brush的像素位置。以下是修改后的完整代码:
{ "$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"}} } ], "signals": [ {"name": "detailDomain"}, {"name": "topBrushTimeRange", "value": null} // 存储顶部Brush的时间区间 ], "marks": [ { "type": "group", "name": "detail", "encode": {"enter": {"height": {"value": 390}, "width": {"value": 720}}}, "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"}, {"orient": "left", "scale": "yDetail"} ], "marks": [ { "name": "detail", "type": "group", "encode": { "enter": { "height": {"field": {"group": "height"}}, "width": {"field": {"group": "width"}}, "clip": {"value": true}, "fill": {"value": "transparent"} } }, "signals": [ { "name": "brushPixels", // 临时存储拖拽时的像素位置 "value": 0, "on": [ {"events": "@detail:pointerdown", "update": "[x(), x()]"}, { "events": "[@detail:pointerdown, window:pointerup] > window:pointermove!", "update": "[brushPixels[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(brushPixels)"}] }, { "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": "topBrushTimeRange", "push": "outer", "on": [ { "events": {"signal": "brushPixels"}, "update": "span(brushPixels) ? invert('xDetail', brushPixels) : null" }, { "events": {"signal": "detailDomain"}, "update": "topBrushTimeRange || null" } ] } ], "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": "rect", "name": "brush", "encode": { "enter": { "y": {"value": 0}, "height": {"value": 390}, "fill": {"value": "#333"}, "fillOpacity": {"value": 0.2} }, "update": { "x": {"signal": "topBrushTimeRange ? scale('xDetail', topBrushTimeRange[0]) : 0"}, "x2": {"signal": "topBrushTimeRange ? scale('xDetail', topBrushTimeRange[1]) : 0"} } } } ] } ] }, { "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) ? 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]"} } } } ] } ] }
关键修改点
- 新增
topBrushTimeRange信号,专门存储顶部Brush选中的时间区间,替代原有的像素位置存储 - 将顶部原
brush信号改名为brushPixels,仅作为拖拽过程中的临时像素容器 - 顶部Brush的位置计算改为基于
topBrushTimeRange和当前xDetail比例尺,确保detailDomain变化时,Brush始终对应同一时间区间 - 监听
detailDomain变化事件,保持topBrushTimeRange值不变,避免平移时丢失选中的时间区间
内容的提问来源于stack exchange,提问作者Vlad
相关产品推荐
相关产品推荐

