From 82aea3e53eae6628f0dd0f4bfac3bd154e5daafa Mon Sep 17 00:00:00 2001 From: xile611 Date: Tue, 15 Sep 2026 18:07:09 +0800 Subject: [PATCH 1/3] fix: synchronize glyph state geometry and restore sankey interactions --- .../fix-sankey-glyph-state-20260915.json | 10 + .../2026-09-15-sankey-glyph-interaction.md | 335 + .../2026-09-15-sankey-glyph-interaction.md | 107 + .../sankey-glyph-results-20260915.json | 5740 +++++++++++++++++ .../__tests__/unit/mark/glyph-state.test.ts | 201 + .../unit/series/fixtures/sankey-related.ts | 51 + .../unit/series/sankey-emphasis-state.test.ts | 78 + packages/vchart/src/mark/base/base-mark.ts | 32 +- packages/vchart/src/mark/box-plot.ts | 6 +- packages/vchart/src/mark/glyph.ts | 284 +- packages/vchart/src/mark/link-path.ts | 14 +- packages/vchart/src/mark/utils/glyph.ts | 5 +- 12 files changed, 6725 insertions(+), 138 deletions(-) create mode 100644 common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json create mode 100644 docs/superpowers/plans/2026-09-15-sankey-glyph-interaction.md create mode 100644 docs/superpowers/validation/2026-09-15-sankey-glyph-interaction.md create mode 100644 docs/superpowers/validation/sankey-glyph-results-20260915.json create mode 100644 packages/vchart/__tests__/unit/mark/glyph-state.test.ts create mode 100644 packages/vchart/__tests__/unit/series/fixtures/sankey-related.ts diff --git a/common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json b/common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json new file mode 100644 index 0000000000..0cb37243a1 --- /dev/null +++ b/common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "修复 Glyph 状态更新未重新编码子图形导致的桑基图 hover 残留、关联流量高亮比例和取消恢复异常;统一共享运行时状态与子图形属性撤销。需要配套包含 Glyph 派生同步接口的 VRender 版本。", + "type": "patch" + } + ], + "packageName": "@visactor/vchart" +} diff --git a/docs/superpowers/plans/2026-09-15-sankey-glyph-interaction.md b/docs/superpowers/plans/2026-09-15-sankey-glyph-interaction.md new file mode 100644 index 0000000000..4b5e327b6c --- /dev/null +++ b/docs/superpowers/plans/2026-09-15-sankey-glyph-interaction.md @@ -0,0 +1,335 @@ +# 桑基图 Glyph 交互恢复 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. +> +> 执行状态:源码修复与本地验收已完成;正式依赖接入等待修复包发布。详见 [验收记录](../validation/2026-09-15-sankey-glyph-interaction.md)。本轮按任务依赖顺序在当前任务内执行。 + +**Goal:** 恢复原桑基图 case 的连线 hover、按流量比例高亮、连续切换和取消选择,并保证状态动画、数据更新及其它 Glyph 图表正常。 + +**Architecture:** VRender 负责统一的状态解析、属性恢复及 Glyph 子图形更新契约;VChart 负责把最终 Glyph 属性编码成连线路径和子图形样式。保留文档承诺的 `glyphStates/glyphStateProxy` 输入,将其适配到现有状态机制;VChart 复用标准状态入口和 Glyph 更新入口。 + +**Tech Stack:** TypeScript、Rush/pnpm、VRender Core/Animate、VChart、Jest、浏览器 Canvas。 + +## Global Constraints + +- VChart 根目录:`/Users/bytedance/Documents/opensource/VChart`。 +- VRender 根目录:`/Users/bytedance/Documents/opensource/VRender`。 +- 静态真值主路径:`baseAttributes + resolvedStatePatch -> attribute`;动画不能写坏静态真值。 +- `normalAttrs` 不再承担 snapshot/restore 核心职责。 +- 标准状态输入保持 Group-first ownership;旧 Glyph 输入的既有优先级单独保留,具体规则见下表,不能改动普通图形的行为。 +- `glyphStates/glyphStateProxy` 是已保留的公开入口;本次不删除,不新增 `subAttributes` 自动传播行为。 +- 优先标准 API;不 monkey-patch 实例状态方法,不向桑基图点击回调添加补偿式属性写入。 +- 高频路径不增加宽泛深比较、全量属性复制、每次交互重新编译状态定义或重复清空再添加状态。 +- 覆盖 10k 图元与多 chart 页面;纯颜色/透明度变化维持 paint-only,几何变化正确更新 bounds/picking。 +- 文档和变更说明优先中文;本计划不包含发布、远端 case 保存或无关重构。 + +## 评审后确定的契约 + +### 状态来源与覆盖顺序 + +| 有效输入 | 定义来源 | 同一属性冲突时的规则 | +| --- | --- | --- | +| 显式配置 `glyphStateProxy` | 旧 Glyph 适配器;proxy 优先于 `glyphStates` | 有 `stateSort` 时按其排序,否则按传入状态顺序;后面的状态覆盖前面的状态 | +| 无 proxy,配置非空 `glyphStates` | 旧 Glyph 适配器 | 同上 | +| 未配置上述旧输入,配置 `states` / Group shared-state | 现有标准定义解析 | 保持现有 Group-first、priority/rank、exclude/suppress 语义 | + +旧输入与 Group 同时存在时,**旧输入仍是该 Glyph 的完整定义来源**,不逐状态混合两套定义;旧输入缺失的状态不隐式回退 Group。这保留原 Glyph 的有效调用行为,是范围明确的兼容规则。移除旧输入后回到标准来源,需重新绑定并刷新当前状态。两种输入都使用同一套状态提交、动画和恢复生命周期。 + +VChart 全部走标准 Group 输入,不再安装 `glyphStateProxy/glyphStates`。共享定义只在 Mark 配置变化时编译,resolver 从 `ctx.graphic` 读取各图元的数据和 `runtimeStateCache[stateName]`;同一状态内的覆盖顺序为 **静态样式 → 数据驱动样式 → 运行时 patch**。不同状态之间仍按 Mark 编译出的优先级覆盖。不能把 ratio 放进被 Group 遮蔽的实例 `states`,也不能让共享 resolver 捕获某一个 Glyph。 + +### 子图形编码与撤销 + +- 编码器读取宿主已提交的最终属性。跟踪每个编码来源实际拥有的输出键;新结果缺少旧键表示撤销该来源的贡献。 +- 显式通道编码优先于位置编码,二者优先于通用继承样式;例如 `backgroundStyle.fill` 优先于 back 的继承 fill,ripple 的派生透明度优先于通用透明度同步。子图形自身的独立属性不能被通用同步覆盖或删除。 +- 撤销后恢复当前仍有效的低优先级贡献、子图形默认值或宿主继承值;不恢复进入状态时的旧快照。若没有自己的值,应删除 own key,让继承/主题默认值生效;写入 `undefined` 不等价于删除。 +- 只比较注册通道和实际拥有的输出键,按顶层值/引用判断变化;不增加逐帧深比较或全量子图形快照。 + +### 更新时序 + +必要的子图形派生同步与可跳过的外部通知分开:**宿主属性提交 → 维护继承绑定 → 派生编码及子图形提交 → 外部通知**。`skipUpdateCallback` 只跳过外部通知,不能留下旧几何。VChart 在子图形及 datum/context 就绪后完成一次初始编码。 + +**评审验收重点:** 反向状态列表的覆盖结果、同 Group 多 Glyph 的不同 ratio、输出键撤销后的低优先级恢复、初始/静默/动画更新时序。比例验收检查实际路径端点厚度,性能验收检查编码和写入次数,不能只看状态名、路径字符串变化或总耗时。 + +--- + +## 1. 基线与已知证据 + +[原 case](https://bugserver.cn.goofy.app/case?product=chartspace4&fileid=64ed94bed4858ce3fbd85afa) 使用层级桑基图、`emphasis: { enable: true, effect: 'related' }`,并配置 `link.state.hover.stroke` 和 `link.state.selected.backgroundStyle`。 + +上一轮浏览器诊断基于 VChart `6cab358b5` 与已安装的 VRender 1.1.8: + +| 操作 | 预期 | 已观察到的失败 | +| --- | --- | --- | +| hover 连线,再移开 | 描边恢复 | `currentStates=[]`,子路径仍有黑色描边 | +| 选择 A → top | top → 00 高亮 15/115 | `ratio` 正确,前景 path 不变,背景 path 为空 | +| 再选择 B → top | 同一连线改为 100/115 | `ratio` 改变,path 仍不变 | +| 点击空白取消 | 状态与外观恢复 | 状态名清空,`ratio/backgroundStyle` 残留 | + +制定计划时,两个仓库均无未提交改动,但 HEAD 已更新为 VChart `351e1d1d6`、VRender `69d82c11b`。VRender 源码包版本是 1.1.9;VChart 实际安装的五个 VRender 包仍为 1.1.8。上述浏览器证据不能直接当成新 HEAD 的测试结果,执行任务 1 时先重新建立基线。 + +源码证据: + +- VRender `packages/vrender-core/src/graphic/glyph.ts`:独立实现 `useStates/clearStates`,未统一维护 `resolvedStatePatch`;旧式恢复不能可靠删除状态新增属性。 +- VRender `packages/vrender-core/src/graphic/graphic.ts`:状态和动画使用 transient/restore 写入路径,不等价于普通 `setAttributes` 的前置钩子。 +- VChart `packages/vchart/src/mark/glyph.ts`:位置和通道编码绑定 `onBeforeAttributeUpdate`,状态补充同步只覆盖基础样式。 +- VRender `setSubGraphic` 将子图形属性原型绑定到宿主属性对象;属性分层会替换对象。需要通过定向测试检查绑定在首次状态、恢复和子图形自身更新后是否仍正确,不能仅靠 VChart 复制颜色掩盖问题。 + +规范入口位于 VRender `docs/refactor/state-engine/D3_ARCH_DESIGN.md`、`D3_UPPER_LAYER_ADOPTION_GUIDE.md` 第 7 节,以及现有状态测试。 + +## 2. 改动边界 + +| 仓库 / 文件 | 责任 | +| --- | --- | +| VRender `packages/vrender-core/src/graphic/glyph.ts` | 旧输入来源/排序适配、标准状态生命周期、派生编码入口和子图形绑定 | +| VRender `packages/vrender-core/src/graphic/graphic.ts` | 提取来源选择和子图形属性删除所需的窄 protected 扩展点;复用属性提交与失效处理 | +| VRender `packages/vrender-core/src/graphic/state/state-engine.ts`、`state-definition.ts` | 为旧 Glyph 适配增加内部输入顺序策略;默认 priority/rank 行为不变 | +| VRender `packages/vrender-core/src/interface/graphic/glyph.ts` | 声明派生编码/批量子图形提交接口,明确它们与 `onUpdate` 的区别 | +| VRender `packages/vrender-core/__tests__/unit/graphic/glyph-state.test.ts` | 状态切换、属性删除、同状态刷新和公开输入兼容 | +| VRender 新增 `packages/vrender-core/__tests__/unit/graphic/glyph-update.test.ts` | 属性提交通知、继承关系和更新类别 | +| VRender `packages/vrender-animate/__tests__/unit/animation-runtime-attribute.test.ts` | 用真实动画推进验证 Glyph 中间帧与结束恢复 | +| VChart `packages/vchart/src/mark/base/base-mark.ts` | 共享 resolver 合入运行时 patch、声明影响键,以及 context 就绪后的初始化扩展点 | +| VChart `packages/vchart/src/mark/glyph.ts` | 统一子图形编码、输出键归属和初始化,删除旧状态安装与重复同步路径 | +| VChart `packages/vchart/src/mark/utils/glyph.ts` | 动态 ratio 更新复用同状态刷新,避免先删除再添加 | +| VChart `packages/vchart/src/mark/link-path.ts` | 预声明高亮状态的运行时 ratio;只修改测试证明需要的路径编码部分 | +| VChart `packages/vchart/__tests__/unit/series/sankey-emphasis-state.test.ts` | 原 case 的事件到最终图形集成回归 | +| VChart 新增 `packages/vchart/__tests__/unit/mark/glyph-state.test.ts` | linkPath、boxPlot、ripple、liquid 的公共 Glyph 编码回归 | +| 两仓库中文文档、`common/changes/` | 记录确定后的更新契约及对应 patch 说明 | + +桑基图路径遍历算法不属于本次根因修复。执行中若暴露独立的 node-link 遍历缺陷,应单独记录,避免扩大这个补丁。 + +## Task 1:固定最小复现和失败断言 + +**Files:** + +- Create: VChart `packages/vchart/__tests__/unit/series/fixtures/sankey-related.ts`。 +- Modify/Test: VChart `packages/vchart/__tests__/unit/series/sankey-emphasis-state.test.ts`。 +- Modify/Test: VRender `packages/vrender-core/__tests__/unit/graphic/glyph-state.test.ts`。 + +**Interfaces:** 使用现有 `findMarkGraphic(rootGroup, eventTarget)` 和 `series.event.emit('pointerdown', { item, mark }, Event_Bubble_Level.chart)`;输出可由后续任务直接运行的回归测试。 + +- [x] 记录两仓库 HEAD、实际包版本和 realpath;不依赖目录名推断加载版本。 +- [x] 新建最小层级 fixture,保留 15/115、100/115 和无关分支: + +```ts +export const createRelatedSpec = () => ({ + type: 'sankey', width: 800, height: 460, + data: [{ values: [{ nodes: [ + { name: 'A', children: [{ name: 'top', children: [{ name: '00', value: 15 }] }] }, + { name: 'B', children: [{ name: 'top', children: [{ name: '00', value: 100 }] }] }, + { name: 'C', children: [{ name: 'bottom', value: 30 }] } + ] }] }], + categoryField: 'name', valueField: 'value', nodeKey: datum => datum.name, + nodeAlign: 'left', nodeWidth: 10, nodeGap: 8, + node: { state: { hover: { fill: 'red' }, blur: { fillOpacity: 0.15 } } }, + link: { state: { + hover: { stroke: '#000000' }, + selected: { backgroundStyle: { fill: '#e8e8e8' } }, + blur: { fill: '#e8e8e8' } + } }, + emphasis: { enable: true, effect: 'related' } +}); +``` + +- [x] 在现有 VChart DOM setup 内渲染 fixture,通过子路径解析真实 mark/item 后发出事件,保存初始前景 path,检查比例、背景与恢复。下面是属性与恢复断言;还必须执行紧随其后的真实厚度断言。`target` 是 `source='top', target='00'` 的 link,`front/back` 来自 `target.getSubGraphic()`: + +```ts +expect(target.attribute.ratio).toBeCloseTo(15 / 115); +expect(front.attribute.path).not.toBe(initialPath); +expect(back.attribute.path).toBe(initialPath); +expect(back.attribute.fill).toBe('#e8e8e8'); +// 再选择 B → top 后: +expect(target.attribute.ratio).toBeCloseTo(100 / 115); +expect(front.attribute.path).not.toBe(pathAfterA); +// 取消后: +expect(target.currentStates).toEqual([]); +expect(target.attribute.ratio).toBeUndefined(); +expect(target.attribute.backgroundStyle).toBeUndefined(); +expect(front.attribute.path).toBe(initialPath); +expect(back.attribute.path).toBe(''); +``` + +- [x] 在同一测试文件增加独立的曲线路径端点读取 helper,限定本 fixture 的无箭头 `M C L C Z` 格式,从命令端点取起点/终点的两侧坐标;不调用生产 `getHorizontalPath/getVerticalPath` 生成期望值。增加 `round: false` 的几何测试变体,断言前景两端厚度都等于 `target.attribute.thickness * ratio`,背景两端厚度都等于完整 thickness,覆盖 15/115、100/115、0、1、取消恢复。原始完整 case 保留默认取整设置,端点厚度按像素取整误差验收。 +- [x] 增加同 Group 两个 Glyph 同时 selected、ratio 各不相同的测试,以及没有配置 selected 样式、只有运行时 ratio 的场景;防止共享 resolver 捕获单个图元或编译器跳过空样式状态。 + +- [x] 在 core 现有 `createTestGlyph` helper 内增加“初始不存在的键”的测试;既检查显示属性也检查静态真值: + +```ts +test('removes attributes introduced only by glyph state', () => { + const { glyph } = createTestGlyph(); + glyph.glyphStates = { + selected: { attributes: { fillOpacity: 0.25 }, subAttributes: [] } + }; + glyph.setStates(['selected'], false); + expect(glyph.attribute.fillOpacity).toBe(0.25); + expect(glyph.baseAttributes.fillOpacity).toBeUndefined(); + glyph.setStates([], false); + expect(glyph.attribute.fillOpacity).toBeUndefined(); + expect(Object.prototype.hasOwnProperty.call(glyph.attribute, 'fillOpacity')).toBe(false); +}); +``` + +- [x] 分别运行以下命令,保存实际失败断言;若新 HEAD 已修复某项,保留该测试并缩减对应实现任务,不预设它必须失败。 + +```sh +# cwd: /Users/bytedance/Documents/opensource/VRender/packages/vrender-core +./node_modules/.bin/jest -c jest.config.js __tests__/unit/graphic/glyph-state.test.ts --runInBand +# cwd: /Users/bytedance/Documents/opensource/VChart/packages/vchart +./node_modules/.bin/jest __tests__/unit/series/sankey-emphasis-state.test.ts --runInBand +``` + +**完成标准:** 测试能区分状态名、实际路径比例、最终属性及删除语义,并记录修复前结果。 + +## Task 2:VRender Glyph 接入统一状态生命周期 + +**Files:** VRender `graphic/glyph.ts`、`graphic/graphic.ts` 窄扩展点、`graphic/state/state-engine.ts`、`graphic/state/state-definition.ts`、`glyph-state.test.ts`。 + +**Interfaces:** 消费 `glyphStates/glyphStateProxy`;对外维持 `setStates(states, options)`、`useStates(states, hasAnimation)`、`clearStates(hasAnimation)`;产出正确的 `currentStates/effectiveStates/resolvedStatePatch` 和最终 `attribute`。 + +- [x] 增加组合状态测试:hover → hover+selected → selected → empty;基准中不存在的键须删除,状态期间更新的 base 值须在清空后保留。 +- [x] 先锁定旧状态排序:`a.stroke=red`、`z.stroke=blue` 时,`useStates(['z', 'a'])` 得到 red,反向列表得到 blue;再配置自定义 `stateSort` 验证覆盖结果,proxy 收到排序后的完整目标列表。标准 `states` 的 priority/rank 排序保持原测试结果,不能把两种规则混为一谈。无需保留旧实现原地排序调用方数组的副作用。 +- [x] 增加同状态动态刷新测试: + +```ts +test('refreshes the same glyph state without clearing it first', () => { + const { glyph } = createTestGlyph(); + let opacity = 0.2; + glyph.glyphStateProxy = () => ({ attributes: { fillOpacity: opacity }, subAttributes: [] }); + glyph.setStates(['selected'], { animate: false }); + opacity = 0.8; + glyph.setStates(['selected'], { animate: false }); + expect(glyph.currentStates).toEqual(['selected']); + expect(glyph.resolvedStatePatch.fillOpacity).toBe(0.8); + expect(glyph.attribute.fillOpacity).toBe(0.8); + expect(glyph.baseAttributes.fillOpacity).toBeUndefined(); +}); +``` + +- [x] 将 Glyph 特有逻辑收敛为状态输入适配:静态 `.attributes` 对应 patch,动态 `.attributes` 对应 resolver;resolver 使用上下文中的实际 graphic,不捕获宿主实例。proxy 返回空时不回退静态定义。纯 proxy 模式支持未在 `glyphStates` 中枚举的目标状态名。 +- [x] 在状态引擎选项中增加仅由旧 Glyph 适配器选择的内部排序策略:默认保持 definition priority/rank;旧输入按目标列表或 `stateSort` 排序后依次合并。复用同一 merge/resolve/commit 过程。禁止通过每次切换重编译 priority/rank 模拟顺序;只在定义引用、proxy、已知状态名集合变化时更新适配缓存,并以测试计数证明反复切换不重编译。 + +- [x] 删除 Glyph 独立的属性合并、快照恢复实现;状态提交、同状态刷新、动画过渡与清空复用 Graphic 主路径。若需要新扩展点,只提取已有解析步骤,不复制第二套状态机。 +- [x] 按契约表测试来源矩阵:proxy 优先、静态旧输入、标准本地输入、标准 Group 输入,以及旧输入与 Group 同时存在时旧输入权威的兼容行为。不做隐式逐状态合并;移除旧输入后切回标准来源,Group 移动、定义更新、clear/release 不留下失效的 scope 登记。 +- [x] 保留公开输入兼容测试;现有测试中“应与普通 Graphic 不同”的描述收窄为“Glyph 专有定义的明确优先级”,不将旧实现方式作为契约。 +- [x] 运行 core 的 glyph-state、state-same-state-refresh、shared-state-refresh、state-update-category 测试。通过后形成独立 VRender 提交。 + +**完成标准:** 清空精确还原静态真值;同名状态可刷新动态 patch;正常用法下没有短暂 normal 状态、属性泄漏或共享状态登记残留。 + +## Task 3:VRender 提供 Glyph 派生同步与继承契约 + +**Files:** VRender `graphic/glyph.ts`、必要的 `graphic/graphic.ts` 批量删除扩展点、`interface/graphic/glyph.ts`、新增 `glyph-update.test.ts`、animate 的 `animation-runtime-attribute.test.ts`。 + +**Interfaces(拟新增,当前源码尚不存在):** + +- `glyph.setSubGraphicEncoder(encoder?: (g: IGlyph, context?: ISetAttributeContext) => void)`:注册/替换必要的派生编码器,并在显式注册时完成一次初始同步;调用方必须已准备好子图形及编码上下文。传入空值解除绑定。后续所有已提交写入都经过此编码器;相同输入可直接跳过子图形写入。 +- `glyph.commitSubGraphicAttributes(subGraphic, patch, removedKeys?, context?)`:提交编码器生成的值及需要删除的 own keys;只修改本 Glyph 所持子图形,复用 Graphic 的 base/state/animation 提交及更新标记。无删除时走已有属性更新路径,不引入全量重建。VChart 决定输出键归属,VRender 负责正确提交。 +- 现有 `onUpdate(cb)` 保留为外部观察入口,位于派生同步之后,遵守 `skipUpdateCallback`;不把 VChart 编码器注册到这里。 + +- [x] 新增测试,要求派生入口在状态写入及清空时读到最终属性;清空观察到键缺失。单独断言注册时的初始同步,后续记录只包含两次有效提交: + +```ts +const seen: unknown[] = []; +glyph.setSubGraphicEncoder(g => seen.push(g.attribute.fillOpacity)); +expect(seen).toEqual([undefined]); +seen.length = 0; +glyph.glyphStates = { + selected: { attributes: { fillOpacity: 0.25 }, subAttributes: [] } +}; +glyph.setStates(['selected'], false); +glyph.setStates([], false); +expect(seen).toEqual([0.25, undefined]); +``` + +- [x] 在 Glyph 自身的属性提交边界完成“绑定维护 → 必要派生同步 → 可跳过的观察回调及 Graphic 服务通知”。覆盖普通设置、`initAttributes`、状态 transient/restore、动画中间帧。不能扩大普通 Graphic 的前置钩子语义,也不能从 render 循环补做编码。 +- [x] 增加 `skipUpdateCallback: true` 测试:子几何正确同步,`onUpdate`、Graphic 服务及自定义事件均不额外通知;向子图形提交时传递该标记。覆盖 render interceptor 的 interactive clone 属性设置,防止渲染期间触发额外通知循环。 +- [x] 编码器只允许写子图形,不在回调中写宿主属性或增删宿主状态;不加每个图元都付费的宽泛重入兜底。clone 保持既有回调不复制语义,子图形初始外观与原件一致;独立注册编码器后只更新 clone。detach/rebind/release 清理自己的绑定,不能持有旧宿主或旧 datum。 +- [x] 用测试锁定宿主属性对象替换后的子图形继承、子图形自身属性覆盖、detach/rebind。只有属性对象身份变化时才重绑原型;正常状态更新不全量复制父属性到所有子图形。 +- [x] 批量撤销测试覆盖:patch 更新与 own key 删除在一次提交中完成;删除后读到当前宿主值或主题默认值;子图形独立值不丢失;子图形自身状态/动画存在时仍从正确静态真值恢复。不能直接由 VChart `delete subGraphic.attribute[key]` 绕过 base、dirty tags 与提交生命周期。 +- [x] 区分几何与绘制更新:纯 fill/fillOpacity 不重新编码 path、不强制所有子图形更新 bounds;ratio/尺寸变化由子图形的新几何触发必要更新。 +- [x] 复用 `animation-runtime-attribute.test.ts` 中的 `createStageHarness`、`bindGraphicService` 和 `tick`,使用真实 ManualTicker 检查 `t=0 / t=mid / t=end` 和中断清空:编码器读到动画当前值,子图形对应中间值,结束后回到静态真值,基础属性保持正确。 +- [x] 跑新增 core 测试和 animate 定向测试,补充 Glyph 更新契约中文说明,形成独立 VRender 提交。 + +**完成标准:** 首帧及所有提交路径的子图形均正确;静默更新保留派生同步且无额外通知;原生 Glyph 的继承与状态恢复本身也正确。 + +## Task 4:VChart 统一 Glyph 编码与运行时状态刷新 + +**Files:** VChart `mark/base/base-mark.ts`、`mark/glyph.ts`、`mark/utils/glyph.ts`、声明运行时影响键的 `mark/link-path.ts`,两个 Glyph/Sankey 回归测试文件。 + +**Interfaces:** 消费任务 2 的标准状态 API 和任务 3 的 `setSubGraphicEncoder/commitSubGraphicAttributes`;位置编码仍使用 `_positionEncoder(glyphAttrs, datum, g)`,通道编码仍使用 `_channelEncoder[channel](value)`。BaseMark 新增 protected `_afterCreateGraphic(g)` 与 `_getRuntimeStateKeys()`:前者默认不做派生工作,后者默认无运行时键声明;Glyph/LinkPath 按需覆盖。 + +- [x] 先完成任务 5 中的本地依赖接入,再运行任务 1 测试,记录“仅 VRender 修复”的剩余失败。 +- [x] BaseMark 创建流程在 `_createGraphic` 返回、赋好 `g.context` 后调用 `_afterCreateGraphic`,Glyph 在此注册一次 `setSubGraphicEncoder` 并完成首轮编码,再进入依赖 bounds/picking 的流程。明确覆盖 progressive 创建路径;不能继续在缺少 datum 的 `_createGraphic` 尾部编码。 +- [x] 移除 `onBeforeAttributeUpdate` 与硬编码状态类型 `2` 的双路同步、子图形空前置钩子,以及 Glyph 的 clear/proxy/useStates 覆盖;状态设置复用 BaseMark 标准路径。 +- [x] 编译时固定需跟踪的输入集合;每个图元只缓存必要输入值与输出键归属。属性存在变为缺失也算变化,采用顶层值/对象引用语义,不支持未承诺的嵌套原地修改。`updateSpecSync` 的 encoder/config 版本变化、图元复用或 datum/context 更换必须使对应缓存失效;读取当前 Mark 编码器和当前图元 context,不捕获初始化 datum。 +- [x] 跟踪位置与各通道对每个子图形的输出贡献,按契约中的优先级合成受影响键。新结果缺少旧键时,撤销该来源并读取当前较低优先级值;没有 own value 时用任务 3 的批量删除入口恢复继承。未由编码器持有的自身属性保持不变;只记录实际占用键,不快照整个子图形。 +- [x] 增加撤销断言:boxPlot angle 从有效值变为缺失时同时撤销 angle/anchor;backgroundStyle 从 `{fill, stroke}` 变为 `{fill}` 时撤销旧 stroke,随后清空时恢复当前宿主 fill;hover stroke 清空后无残留。状态期间更新基础 fill/几何,取消后展示新基础值。 +- [x] 清空 ratio 使 front 恢复全宽、back path 为空;ripple 的专用透明度不被通用样式覆盖;liquid 纯 fill 更新不重建点数组。除位置输入或编码上下文变化外不重复调用位置 encoder。 +- [x] 修改 BaseMark `_applySharedStateDefinitions`:状态名取样式编码状态与 `_getRuntimeStateKeys()` 声明状态的并集;LinkPath 预声明 `STATE_SANKEY_EMPHASIS` 对应的 `ratio`。没有静态/动态样式但声明运行时键的状态也生成定义。普通 Mark 没有运行时键时保留既有路径。 +- [x] 共享 resolver 从 `ctx.graphic` 读取 datum 和运行时 patch;同一状态内按“静态 patch → 数据驱动 resolver → runtime patch”合并。`declaredAffectedKeys` 包含动态键与运行时键的并集,编译 cache key 包含运行时声明及 encoder 配置。定义由 Mark 共享,解析结果由各 Graphic 缓存,ratio 更新不替换共享定义。 +- [x] 为共享来源补测试:两个 Glyph 的 ratio 互不污染;静态背景色、数据驱动属性和 runtime 同时生效;同属性冲突由 runtime 覆盖;只有 ratio 的状态不被跳过;普通 Mark 状态行为不变。不能以实例 `g.states` 补运行时 patch。 +- [x] `addRuntimeState` 更新缓存后一次性调用现有标准刷新入口,不再先 remove 再 add;保留其它活跃状态,由现有 `setGraphicStates` 传入支持同状态刷新及动画的 options: + +```ts +g.runtimeStateCache ??= {}; +g.runtimeStateCache[stateName] = attrs; +addGraphicState(g, stateName, keepCurrentStates, hasAnimation); +``` + +- [x] 测试连续 A → B → A、hover 与 selected 叠加后仅移除 hover、点击空白、`updateDataSync/updateSpecSync` 后重新交互;保留已有 adjacency 子路径拾取及初始单路径透明度测试。 +- [x] 为 boxPlot、ripple、liquid 补齐上述对应的公共编码验证,包含首帧、几何变化、专用样式与 clear;添加图元复用后旧 ratio 不会复活的用例。运行时缓存按图元数据生命周期处理,不为每次 hover 全量清理。 +- [x] 跑定向 VChart 测试并形成一个独立 VChart 提交,不在桑基图 handler 中额外刷新图形。 + +**完成标准:** 原 case 四项行为同时恢复,实际路径厚度和子图形属性均正确;多 Glyph 共享定义、普通数据更新、其它 Glyph 图表无回归。 + +## Task 5:跨仓库接入、原 case 与性能验收 + +**Files:** 两仓库测试和中文 patch 变更记录;VChart 依赖与锁文件只在正式接入修复版本时更新。 + +**Interfaces:** VRender 同一源码提交构建出的 `vrender/core/kits/components/animate` 五个包;VChart 实际解析路径与包版本必须可追溯。 + +- [x] 在 VRender 通过现有 Rush 构建依赖闭包: + +```sh +# cwd: /Users/bytedance/Documents/opensource/VRender +rush build -t @visactor/vrender +``` + +- [x] 创建 `/tmp/vrender-glyph-fix`,将五个包分别在各自包目录执行 `npm pack --pack-destination /tmp/vrender-glyph-fix`,记录 tarball 名称、校验和与源码提交。通过 Rush 的 `common/config/rush/pnpm-config.json` 临时 `globalOverrides` 统一指向这批本地产物,再 `rush update`;禁止只替换顶层 core 而保留传递依赖里的另一份 core。临时路径不提交。 +- [x] 用 Node `createRequire` 从 VChart 及五个包分别解析 `@visactor/vrender-core`,比较 realpath;确认单一 core 实例来源,并在浏览器产物记录构建标识。 +- [x] 在同一原 case 上验证三组构建:旧依赖基线、仅修复 VRender、完整修复。前两组用于归因,完整修复须通过全部验收。 +- [x] 浏览器使用原始完整 spec、真实鼠标输入,验证节点与连线 hover、移出、点击选择、连续切换和空白清空;再验证 `emphasis.trigger: 'hover'`、纵向布局和已有 adjacency 场景。截图与路径端点厚度/own key/状态属性结构断言同时保留,不能只检查 handler、状态名或 path 不相等。 +- [x] 完成功能与类型检查: + +```sh +# cwd: /Users/bytedance/Documents/opensource/VRender/packages/vrender-core +./node_modules/.bin/jest -c jest.config.js __tests__/unit/graphic --runInBand +./node_modules/.bin/tsc --noEmit +# cwd: /Users/bytedance/Documents/opensource/VRender/packages/vrender-animate +./node_modules/.bin/jest -c jest.config.js __tests__/unit/animation-runtime-attribute.test.ts --runInBand +./node_modules/.bin/tsc --noEmit +# cwd: /Users/bytedance/Documents/opensource/VRender/packages/vrender +./node_modules/.bin/jest -c jest.config.js __tests__/graphic/graphic-state.test.ts --runInBand +./node_modules/.bin/tsc --noEmit +# cwd: /Users/bytedance/Documents/opensource/VChart/packages/vchart +./node_modules/.bin/jest __tests__/unit/series/sankey-emphasis-state.test.ts __tests__/unit/series/sankey-link-opacity.test.ts __tests__/unit/mark/glyph-state.test.ts __tests__/unit/interaction --runInBand +./node_modules/.bin/tsc --noEmit +``` + +- [x] 对实际改动文件执行 ESLint/Prettier 检查;两个仓库执行 `git diff --check`。若 shared-state 公共扩展点影响 React 绑定,追加 react-vrender 的状态/生命周期测试及 compile,不自动扩大到无关测试矩阵。 +- [x] 性能检查分两类:10k 个 Glyph(20k 子路径)的连续状态切换,及多 chart 的 render/update/release。固定机器与构建条件,预热 2 次、测量 7 次,报告中位数、范围、调用次数及实际依赖。 +- [x] 在测试/基准中计数,不向生产热路径加遥测:状态编译次数、resolver 次数、位置/通道 encoder 次数、实际子图形属性提交次数。10k Glyph 重复交互不重新编译共享定义;纯颜色切换的位置 encoder 为 0;相同状态与相同 patch 的子图形提交为 0;无动画的同状态 ratio 变化每个受影响 Glyph 编码一次,每个真正变化的子图形至多提交一次,不中途恢复 normal。 +- [x] 动画按每次实际属性提交检查派生同步,不强求整个过渡只编码一次。性能差异须与测量波动区分;稳定变慢时定位分配/编译/重绘成本,不能以功能通过代替性能验收。 +- [x] 按仓库流程生成中文 patch 说明;移除自己引入的临时 overrides 和锁文件变更,保留其它工作区改动。 +- [ ] 待修复包发布后,VChart 正式依赖采用包含本修复的实际发布版本并通过 Rush 更新锁文件。本任务不直接执行发布,不能把本地源码版本号当作发布完成。 + +**完成标准:** 原 case 真实操作通过;core、上层、类型、代码规范检查通过;依赖来源唯一且可追溯;性能无未解释退化;临时联调配置不进入提交。 + +## 3. 执行顺序与最终交付 + +1. 任务 1 固定基线。 +2. 任务 2 修复状态真值;任务 3 修复 Glyph 更新契约。 +3. 任务 5 的本地构建/接入步骤先完成,再做任务 4。 +4. 任务 5 完成最终验收与交付记录。 + +**合入门槛:** 状态来源/覆盖顺序、共享 ratio、输出键撤销、派生同步时序这四组回归全部通过;原 case 有真实厚度证据;10k/多 chart 的调用次数和耗时无未解释退化。不能仅凭点击视觉恢复合入。 + +交付包括 VRender 与 VChart 可分别审查的提交、失败到通过的测试证据、原 case 对照截图/结构输出、性能结果及实际依赖版本。运行时修复与验证完成前,不以计划完成替代问题修复完成。 diff --git a/docs/superpowers/validation/2026-09-15-sankey-glyph-interaction.md b/docs/superpowers/validation/2026-09-15-sankey-glyph-interaction.md new file mode 100644 index 0000000000..6dab7f6e63 --- /dev/null +++ b/docs/superpowers/validation/2026-09-15-sankey-glyph-interaction.md @@ -0,0 +1,107 @@ +# 桑基图 Glyph 交互修复验收 + +## 结果与接入条件 + +原始完整 case 的点击比例高亮、hover 描边、连续切换和取消恢复已通过真实鼠标验证。VRender 与 VChart 都需要修复;只更新 VRender 仍不会重新编码前景/背景路径。 + +**正式接入尚待 VRender 修复包发布。VChart 不能单独合入后继续使用锁定的 1.1.8。** 本轮不发布包,也未把本地 tarball 地址提交到 Rush 配置。发布后须将五个 VRender 依赖的最低版本及锁文件更新到包含修复的实际版本,再进行安装验证。 + +- 分支:两仓库均为 `codex/fix-sankey-glyph-state`。 +- 执行基线:VChart `702ecbb49`、VRender `69d82c11b`;实际旧包为 1.1.8。 +- VRender 提交:`c828aff84`(统一状态解析)、`775a4a4d6`(派生同步和动画中断)、`880cc3c8d`(子图形 paint 分类)。 +- 联调产物:从 `880cc3c8d` 构建的五个本地 1.1.9 tarball;**此版本号仅表示本地源码版本,不表示发布包已包含修复。** +- VChart 与五个包解析出的 core realpath 完全相同。校验和、版本、路径及逐次性能数据见 [原始验收数据](sankey-glyph-results-20260915.json)。 +- 原 case:[64ed94bed4858ce3fbd85afa](https://bugserver.cn.goofy.app/case?product=chartspace4&fileid=64ed94bed4858ce3fbd85afa)。 + +## 根因与修改 + +1. Glyph 旧状态实现绕过统一静态真值和恢复流程。保留旧公开输入与排序语义,适配到现有状态引擎。 +2. VChart 在属性提交前编码,状态/动画提交没有完整经过该路径。改为在最终属性提交后编码;context 就绪后初始化,静默写入仍同步必要几何。 +3. 子图形属性对象在状态切换时会替换。维护宿主继承绑定,并按输出来源撤销 own keys,恢复当前较低优先级贡献。 +4. runtime ratio 进入 Group 共享 resolver,按实际 graphic 读取。同状态刷新只提交一次。 +5. 中断状态动画时,不再隐式执行公开 `stop('end')` 的静态终值提交,避免污染 base;显式 stop API 保持原语义。 + +## 功能验证 + +| 检查 | 结果 | +| --- | --- | +| 修复前 core 回归 | 4 项失败,涉及新增键删除、动态刷新、顺序及标准 Group 输入 | +| 旧依赖原 case | ratio 改变但前景仍全宽、背景为空;清空后 ratio/backgroundStyle 与 1 条 hover 描边残留 | +| 只修 VRender | ratio 正确,但实际前景仍全宽、背景为空 | +| 完整修复,水平 A → B | 前景两端均为 18px → 123px,背景全宽 142px,符合 15/115 → 100/115 的取整结果 | +| 完整修复,纵向 A → B | 前景两端均为 34px → 228px,背景全宽 262px | +| 点击空白 | 所有链接状态清空、描边残留为 0,ratio/backgroundStyle 消失、前景全宽、背景为空 | +| 节点 hover / 移出 | A 填充 red → 原始 #1664FF | +| hover 触发关联 / 移出 | 比例正确,并恢复全宽 | +| adjacency | 当前链接 selected,其余链接按配置 blur | + +浏览器结构输出保存在验收数据中;截图已在本次任务中展示。最终浏览器构建标识为 `glyph-fix-880cc3c8d`。没有修改或保存远端 case。 + +## 自动检查 + +- VRender core:28 suites / **188 tests 通过**。 +- VRender animate:**46 tests 通过**,包含真实 ManualTicker 的 Glyph 中间帧、取消及中断恢复。 +- VRender 公开状态入口:**9 tests 通过**。 +- VChart:5 suites / **20 tests 通过**;包括两个方向的实际路径端点、0/1、不同 Glyph 的 ratio、同状态刷新、输出键撤销、boxPlot/ripple/liquid、data/spec 更新、图元复用及渐进创建。 +- core / animate / vrender / vchart 的 `tsc --noEmit` 通过。 +- `rush build -t @visactor/vrender` 通过;VChart 浏览器 bundle 构建通过。 +- 改动源码 ESLint 无 error(有 124 条 warning,主要是 any 类型及重复 import),Prettier 和两仓库 `git diff --check` 通过。 + +命令和测试入口沿用 [执行计划](../plans/2026-09-15-sankey-glyph-interaction.md#task-5跨仓库接入原-case-与性能验收)。本轮没有修改 React 绑定;未扩展 React 测试矩阵。 + +## 性能与成本 + +同一台机器、同一浏览器 Chromium 152、本地非压缩构建;预热 2 次、记录 7 次。每个 Glyph 使用独立属性对象,10k Glyph 对应 20k 子路径。统计仅在基准中包装方法,没有加入生产代码。表中为毫秒,中位数 [最小–最大]。 + +### 10k Glyph + +| 操作 | 旧 VChart + VRender 1.1.8 | 完整修复 | +| --- | --- | --- | +| 创建 | 43.0 [42.0–46.0] | 40.0 [39.2–54.9] | +| 全量几何更新 | 17.9 [15.2–19.5] | 24.5 [19.7–31.5] | +| 基础颜色更新 | 13.6 [13.3–14.5] | 10.3 [9.3–12.8] | +| 首次 hover | 16.4 [14.2–18.0] | 22.0 [20.4–24.9] | +| 重复 hover | 14.0 [13.0–15.2] | 22.4 [19.6–24.2] | +| 重复 hover 清空 | 13.5 [12.8–14.8] | 17.1 [15.1–18.5] | +| 同状态 ratio 改变 | 25.4 [24.6–26.2] | 49.3 [45.2–56.2] | +| 相同 ratio 刷新 | 24.1 [23.3–24.6] | 10.8 [10.7–12.9] | +| 释放 | 1.0 [0.6–1.3] | 10.4 [8.2–11.2] | + +固定 10k 图元的完整状态扫描有明确额外成本,不能宣称所有路径都变快: + +- 几何更新约增加 6.6ms。新路径维护来源输出键、撤销与属性分类,实际子图形写入由 20k 降为 10k;这项属性维护成本仍存在。已避免处理不变输出和为未变化子路径分配 patch。 +- hover 通过统一状态引擎及静态真值提交;重复 hover 仍比旧实现慢。旧实现的清空会留下样式,因此旧流程不能作为等价正确行为的性能目标。 +- ratio 改变现在真正计算并写入 10k 条前景路径;旧流程没有执行这些必要工作,不应把两者耗时直接当作性能回退比例。 +- release 现在同时释放 20k 子图形、解绑原型与编码器;旧 Glyph release 没有完成同等清理。 +- 以上是全量 10k 图元操作的实际代价,后续大规模 Glyph 优化应保留本次恢复和删除契约。这里不作 60FPS 保证。 + +调用次数(一次扫描全部 10k Glyph): + +| 操作 | 重编译 | resolver | 位置编码 | 通道编码 | 子图形提交 | +| --- | ---: | ---: | ---: | ---: | ---: | +| 纯色 hover | 0 | 0 | 0 | 0 | 0 | +| 初次 selected | 0 | 10000 | 10000 | 10000 | 20000 | +| ratio 改变 | 0 | 10000 | 10000 | 0 | 10000 | +| 相同 ratio 刷新 | 0 | 10000 | 0 | 0 | 0 | + +### 标准页面 + +复用仓库 `render-performance.ts` 中的 scatter-10k 和 120 charts 场景,后者补测 `updateSpecSync` 改宽度。先 baseline→fixed,再 fixed→baseline;下表为第二轮,第一轮原始数据也已保留。 + +| 场景 / 阶段 | 基线 | 修复 | +| --- | --- | --- | +| scatter-10k / createMs | 0.8 [0.7–1.0] | 0.8 [0.6–0.8] | +| scatter-10k / renderMs | 38.6 [35.3–45.4] | 39.0 [35.1–41.3] | +| scatter-10k / releaseMs | 2.8 [2.3–3.9] | 2.2 [1.8–2.8] | +| multi-chart-grid / createMs | 52.6 [31.9–78.9] | 52.2 [31.3–77.0] | +| multi-chart-grid / renderMs | 50.7 [48.3–65.8] | 49.7 [48.1–53.2] | +| multi-chart-grid / updateMs | 60.3 [58.7–66.1] | 60.6 [55.0–65.4] | +| multi-chart-grid / releaseMs | 4.1 [3.9–5.6] | 4.4 [3.9–5.1] | + +两轮普通图表页面的耗时范围有重叠,反转测量顺序后没有稳定退化信号;不能将第一轮约数毫秒差异解释为确定的收益或损失。上述结论仅覆盖这台机器和这些场景。 + +## 本地复查与清理 + +- 临时构建、日志、浏览器验证页和基准脚本位于 `/tmp/vrender-glyph-fix`;tarball 位于其 `r3` 子目录。 +- 当前本地 node_modules 保留已验证的 tarball,便于继续审查。仓库中的临时 overrides / 锁文件变更已恢复;再次执行 Rush install/update 会按正式依赖恢复安装,发布接入前不能据此声称修复包已正式可用。 +- 复查本地联调时,临时将五个 `globalOverrides` 统一指向 `r3` 中的 tarball,运行 `rush update`,再运行计划列出的测试;不要只替换顶层 core。正式接入时移除临时覆盖并使用实际发布版本。 diff --git a/docs/superpowers/validation/sankey-glyph-results-20260915.json b/docs/superpowers/validation/sankey-glyph-results-20260915.json new file mode 100644 index 0000000000..2eb63327a9 --- /dev/null +++ b/docs/superpowers/validation/sankey-glyph-results-20260915.json @@ -0,0 +1,5740 @@ +{ + "build-metadata": { + "vrenderCommit": "880cc3c8d", + "vchartBaseline": "702ecbb49", + "vrenderBaseline": "69d82c11b", + "oldPackageVersion": "1.1.8", + "localPackageVersion": "1.1.9", + "tarballs": { + "visactor-vrender-components-1.1.9.tgz": "2dd0de6b2b30745b9fae8b64e0ba524b9fd75a400737bdd009aee03d0dd40fd6", + "visactor-vrender-kits-1.1.9.tgz": "ad7dcd5759a48387cfd8fc038d4556707d546c6d21945c8bed1121975ef5e07d", + "visactor-vrender-core-1.1.9.tgz": "e4165e46b177703ff93fd9c8567316f5d78ec47a4765d75bbaa2a35c87c0aec0", + "visactor-vrender-1.1.9.tgz": "6b44cc43920c3541f83ea86e4c502e020a251864209c47f95c9762aa014ec2fa", + "visactor-vrender-animate-1.1.9.tgz": "c3400b454c9b975f705eb318f83c4107e08c9dea5574bc909c6df71242f55781" + } + }, + "dependencies-final": [ + { + "name": "vchart", + "core": "/Users/bytedance/Documents/opensource/VChart/common/temp/node_modules/.pnpm/@visactor+vrender-core@file+..+..+..+..+..+..+..+tmp+vrender-glyph-fix+r3+visactor-vrender-core-1.1.9.tgz/node_modules/@visactor/vrender-core/cjs/index.js" + }, + { + "name": "vrender", + "version": "1.1.9", + "core": "/Users/bytedance/Documents/opensource/VChart/common/temp/node_modules/.pnpm/@visactor+vrender-core@file+..+..+..+..+..+..+..+tmp+vrender-glyph-fix+r3+visactor-vrender-core-1.1.9.tgz/node_modules/@visactor/vrender-core/cjs/index.js" + }, + { + "name": "vrender-core", + "version": "1.1.9", + "core": "/Users/bytedance/Documents/opensource/VChart/common/temp/node_modules/.pnpm/@visactor+vrender-core@file+..+..+..+..+..+..+..+tmp+vrender-glyph-fix+r3+visactor-vrender-core-1.1.9.tgz/node_modules/@visactor/vrender-core/cjs/index.js" + }, + { + "name": "vrender-kits", + "version": "1.1.9", + "core": "/Users/bytedance/Documents/opensource/VChart/common/temp/node_modules/.pnpm/@visactor+vrender-core@file+..+..+..+..+..+..+..+tmp+vrender-glyph-fix+r3+visactor-vrender-core-1.1.9.tgz/node_modules/@visactor/vrender-core/cjs/index.js" + }, + { + "name": "vrender-components", + "version": "1.1.9", + "core": "/Users/bytedance/Documents/opensource/VChart/common/temp/node_modules/.pnpm/@visactor+vrender-core@file+..+..+..+..+..+..+..+tmp+vrender-glyph-fix+r3+visactor-vrender-core-1.1.9.tgz/node_modules/@visactor/vrender-core/cjs/index.js" + }, + { + "name": "vrender-animate", + "version": "1.1.9", + "core": "/Users/bytedance/Documents/opensource/VChart/common/temp/node_modules/.pnpm/@visactor+vrender-core@file+..+..+..+..+..+..+..+tmp+vrender-glyph-fix+r3+visactor-vrender-core-1.1.9.tgz/node_modules/@visactor/vrender-core/cjs/index.js" + } + ], + "baseline-render-round1": { + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36", + "options": { + "samples": 7, + "warmup": 2, + "scenarios": [ + "scatter-10k", + "multi-chart-grid" + ] + }, + "scenarios": [ + { + "name": "scatter-10k", + "samples": [ + { + "totalMs": 50.40000009536743, + "createMs": 0.9000000953674316, + "renderMs": 43.40000009536743, + "releaseMs": 3.1999998092651367, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 3.0999999046325684, + "compileToVGrammar": 0.10000014305114746, + "vRenderDraw": 6.1000001430511475, + "doRender": 6.300000190734863 + } + }, + { + "totalMs": 49.30000019073486, + "createMs": 0.7999999523162842, + "renderMs": 43.299999952316284, + "releaseMs": 3.0999999046325684, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 3.5, + "compileToVGrammar": 0.09999990463256836, + "vRenderDraw": 5.200000047683716, + "doRender": 5.299999952316284 + } + }, + { + "totalMs": 49.90000009536743, + "createMs": 0.7000000476837158, + "renderMs": 39.799999952316284, + "releaseMs": 2.700000047683716, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 3.1000001430511475, + "compileToVGrammar": 0, + "vRenderDraw": 4.8999998569488525, + "doRender": 5.299999952316284 + } + }, + { + "totalMs": 50, + "createMs": 0.7000000476837158, + "renderMs": 37.299999952316284, + "releaseMs": 2.5999999046325684, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.6999998092651367, + "compileToVGrammar": 0.10000014305114746, + "vRenderDraw": 4.400000095367432, + "doRender": 4.5 + } + }, + { + "totalMs": 49.89999985694885, + "createMs": 0.9000000953674316, + "renderMs": 41.799999952316284, + "releaseMs": 2.5, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 3.5, + "compileToVGrammar": 0.09999990463256836, + "vRenderDraw": 4.599999904632568, + "doRender": 4.799999952316284 + } + }, + { + "totalMs": 50.09999990463257, + "createMs": 0.5999999046325684, + "renderMs": 37.700000047683716, + "releaseMs": 2.0999999046325684, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.5, + "compileToVGrammar": 0, + "vRenderDraw": 4.400000095367432, + "doRender": 4.700000047683716 + } + }, + { + "totalMs": 50.09999990463257, + "createMs": 0.6999998092651367, + "renderMs": 39.30000019073486, + "releaseMs": 2.0999999046325684, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.299999952316284, + "compileToVGrammar": 0, + "vRenderDraw": 6, + "doRender": 6.299999952316284 + } + } + ] + }, + { + "name": "multi-chart-grid", + "samples": [ + { + "totalMs": 168.29999995231628, + "createMs": 31, + "renderMs": 57.299999952316284, + "releaseMs": 4.700000047683716, + "updateMs": 69.59999990463257, + "longTaskCount": 1, + "longTaskMs": 163, + "phaseMs": { + "initializeChart": 20.000000715255737, + "compileToVGrammar": 2.700000524520874, + "vRenderDraw": 10.09999966621399, + "doRender": 10.799999475479126 + } + }, + { + "totalMs": 183.29999995231628, + "createMs": 38.19999980926514, + "renderMs": 53.90000009536743, + "releaseMs": 4.900000095367432, + "updateMs": 69, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 17.80000066757202, + "compileToVGrammar": 2.999999761581421, + "vRenderDraw": 10.900000810623169, + "doRender": 11.300000667572021 + } + }, + { + "totalMs": 199.30000019073486, + "createMs": 47.700000047683716, + "renderMs": 65.60000014305115, + "releaseMs": 6, + "updateMs": 68, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 21.999999046325684, + "compileToVGrammar": 2.8999993801116943, + "vRenderDraw": 12.999999284744263, + "doRender": 13.499999523162842 + } + }, + { + "totalMs": 183.39999985694885, + "createMs": 51.19999980926514, + "renderMs": 52.5, + "releaseMs": 4.400000095367432, + "updateMs": 62.200000047683716, + "longTaskCount": 1, + "longTaskMs": 170, + "phaseMs": { + "initializeChart": 16.899999380111694, + "compileToVGrammar": 2.200000286102295, + "vRenderDraw": 10.599998712539673, + "doRender": 10.999998569488525 + } + }, + { + "totalMs": 183.5, + "createMs": 59.5, + "renderMs": 50.200000047683716, + "releaseMs": 4, + "updateMs": 59.299999952316284, + "longTaskCount": 1, + "longTaskMs": 173, + "phaseMs": { + "initializeChart": 15.200001001358032, + "compileToVGrammar": 2.499999761581421, + "vRenderDraw": 11.69999885559082, + "doRender": 12.6999990940094 + } + }, + { + "totalMs": 200, + "createMs": 67.20000004768372, + "renderMs": 53.5, + "releaseMs": 4.400000095367432, + "updateMs": 60.799999952316284, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 16.399999618530273, + "compileToVGrammar": 3.200000047683716, + "vRenderDraw": 9.700000524520874, + "doRender": 10.100000381469727 + } + }, + { + "totalMs": 200.29999995231628, + "createMs": 76.09999990463257, + "renderMs": 53, + "releaseMs": 4.200000047683716, + "updateMs": 58.200000047683716, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 16.200001001358032, + "compileToVGrammar": 1.6999986171722412, + "vRenderDraw": 10.300000190734863, + "doRender": 11.000000238418579 + } + } + ] + } + ] + }, + "fixed-render-round1": { + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36", + "options": { + "samples": 7, + "warmup": 2, + "scenarios": [ + "scatter-10k", + "multi-chart-grid" + ] + }, + "scenarios": [ + { + "name": "scatter-10k", + "samples": [ + { + "totalMs": 50, + "createMs": 0.7999999523162842, + "renderMs": 43.200000047683716, + "releaseMs": 2.299999952316284, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 3.200000047683716, + "compileToVGrammar": 0.09999990463256836, + "vRenderDraw": 7.800000190734863, + "doRender": 7.900000095367432 + } + }, + { + "totalMs": 49.89999985694885, + "createMs": 0.7000000476837158, + "renderMs": 37.700000047683716, + "releaseMs": 2.3999998569488525, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.5999999046325684, + "compileToVGrammar": 0.20000004768371582, + "vRenderDraw": 4.400000095367432, + "doRender": 4.5 + } + }, + { + "totalMs": 49, + "createMs": 0.7999999523162842, + "renderMs": 43.5, + "releaseMs": 2.8000001907348633, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 4, + "compileToVGrammar": 0.09999990463256836, + "vRenderDraw": 4.8999998569488525, + "doRender": 5.099999904632568 + } + }, + { + "totalMs": 49.89999985694885, + "createMs": 0.7000000476837158, + "renderMs": 38.5, + "releaseMs": 2.5, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 3, + "compileToVGrammar": 0.10000014305114746, + "vRenderDraw": 4.800000190734863, + "doRender": 5.1000001430511475 + } + }, + { + "totalMs": 49.90000009536743, + "createMs": 0.7000000476837158, + "renderMs": 40.799999952316284, + "releaseMs": 2, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.700000047683716, + "compileToVGrammar": 0, + "vRenderDraw": 6.199999809265137, + "doRender": 6.599999904632568 + } + }, + { + "totalMs": 50, + "createMs": 0.7000000476837158, + "renderMs": 35.200000047683716, + "releaseMs": 2, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.4000000953674316, + "compileToVGrammar": 0.09999990463256836, + "vRenderDraw": 4.299999952316284, + "doRender": 4.400000095367432 + } + }, + { + "totalMs": 49.799999952316284, + "createMs": 0.7999999523162842, + "renderMs": 41.80000019073486, + "releaseMs": 2.299999952316284, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 3.799999952316284, + "compileToVGrammar": 0, + "vRenderDraw": 4.200000047683716, + "doRender": 4.3999998569488525 + } + } + ] + }, + { + "name": "multi-chart-grid", + "samples": [ + { + "totalMs": 166.70000004768372, + "createMs": 31.299999952316284, + "renderMs": 57.200000047683716, + "releaseMs": 4.400000095367432, + "updateMs": 65.19999980926514, + "longTaskCount": 1, + "longTaskMs": 158, + "phaseMs": { + "initializeChart": 19.90000033378601, + "compileToVGrammar": 2.699998140335083, + "vRenderDraw": 9.099998712539673, + "doRender": 9.799998998641968 + } + }, + { + "totalMs": 183.29999995231628, + "createMs": 39.59999990463257, + "renderMs": 58.40000009536743, + "releaseMs": 4.900000095367432, + "updateMs": 63.799999952316284, + "longTaskCount": 1, + "longTaskMs": 167, + "phaseMs": { + "initializeChart": 20.399998426437378, + "compileToVGrammar": 3.8000004291534424, + "vRenderDraw": 13.09999966621399, + "doRender": 13.699999809265137 + } + }, + { + "totalMs": 183.40000009536743, + "createMs": 48.5, + "renderMs": 65.59999990463257, + "releaseMs": 6.200000047683716, + "updateMs": 60.10000014305115, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 24.100001335144043, + "compileToVGrammar": 2.9999990463256836, + "vRenderDraw": 13.500000953674316, + "doRender": 13.900001049041748 + } + }, + { + "totalMs": 183.39999985694885, + "createMs": 53.39999985694885, + "renderMs": 51.5, + "releaseMs": 4.599999904632568, + "updateMs": 65.90000009536743, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 16.000000476837158, + "compileToVGrammar": 2.1999998092651367, + "vRenderDraw": 9.200000762939453, + "doRender": 9.80000114440918 + } + }, + { + "totalMs": 199.09999990463257, + "createMs": 66.89999985694885, + "renderMs": 53.40000009536743, + "releaseMs": 4.400000095367432, + "updateMs": 61.09999990463257, + "longTaskCount": 1, + "longTaskMs": 186, + "phaseMs": { + "initializeChart": 17.099998712539673, + "compileToVGrammar": 1.700000286102295, + "vRenderDraw": 11.600000619888306, + "doRender": 12.100000381469727 + } + }, + { + "totalMs": 249.89999985694885, + "createMs": 89, + "renderMs": 72, + "releaseMs": 4.700000047683716, + "updateMs": 76.79999995231628, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 23.79999876022339, + "compileToVGrammar": 4.400000095367432, + "vRenderDraw": 14.299999713897705, + "doRender": 14.899999618530273 + } + }, + { + "totalMs": 233.29999995231628, + "createMs": 88.60000014305115, + "renderMs": 58.59999990463257, + "releaseMs": 4.200000047683716, + "updateMs": 66.40000009536743, + "longTaskCount": 1, + "longTaskMs": 218, + "phaseMs": { + "initializeChart": 18.90000081062317, + "compileToVGrammar": 3.1000003814697266, + "vRenderDraw": 10.899999856948853, + "doRender": 11.09999966621399 + } + } + ] + } + ] + }, + "baseline-render": { + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36", + "options": { + "samples": 7, + "warmup": 2, + "scenarios": [ + "scatter-10k", + "multi-chart-grid" + ] + }, + "scenarios": [ + { + "name": "scatter-10k", + "samples": [ + { + "totalMs": 49, + "createMs": 0.9000000953674316, + "renderMs": 42.5, + "releaseMs": 3.8999998569488525, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.9000000953674316, + "compileToVGrammar": 0.09999990463256836, + "vRenderDraw": 6.6000001430511475, + "doRender": 6.900000095367432 + } + }, + { + "totalMs": 49.30000019073486, + "createMs": 0.7999999523162842, + "renderMs": 38.60000014305115, + "releaseMs": 2.799999952316284, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.5999999046325684, + "compileToVGrammar": 0.10000014305114746, + "vRenderDraw": 5, + "doRender": 5.1000001430511475 + } + }, + { + "totalMs": 49.799999952316284, + "createMs": 0.8000001907348633, + "renderMs": 38.299999952316284, + "releaseMs": 2.799999952316284, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.5, + "compileToVGrammar": 0, + "vRenderDraw": 4.900000095367432, + "doRender": 5 + } + }, + { + "totalMs": 65.59999990463257, + "createMs": 1, + "renderMs": 45.39999985694885, + "releaseMs": 2.8000001907348633, + "longTaskCount": 1, + "longTaskMs": 50, + "phaseMs": { + "initializeChart": 3.9000000953674316, + "compileToVGrammar": 0.09999990463256836, + "vRenderDraw": 5.099999904632568, + "doRender": 5.199999809265137 + } + }, + { + "totalMs": 50, + "createMs": 0.7999999523162842, + "renderMs": 39, + "releaseMs": 2.4000000953674316, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.5999999046325684, + "compileToVGrammar": 0.10000014305114746, + "vRenderDraw": 4.599999904632568, + "doRender": 4.900000095367432 + } + }, + { + "totalMs": 50, + "createMs": 0.7999999523162842, + "renderMs": 36, + "releaseMs": 2.3000001907348633, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.200000047683716, + "compileToVGrammar": 0.09999990463256836, + "vRenderDraw": 4.3999998569488525, + "doRender": 4.699999809265137 + } + }, + { + "totalMs": 48.299999952316284, + "createMs": 0.7000000476837158, + "renderMs": 35.299999952316284, + "releaseMs": 2.3000001907348633, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.3000001907348633, + "compileToVGrammar": 0, + "vRenderDraw": 4.599999904632568, + "doRender": 4.8999998569488525 + } + } + ] + }, + { + "name": "multi-chart-grid", + "samples": [ + { + "totalMs": 183.40000009536743, + "createMs": 31.90000009536743, + "renderMs": 65.79999995231628, + "releaseMs": 4.6000001430511475, + "updateMs": 64.29999995231628, + "longTaskCount": 1, + "longTaskMs": 167, + "phaseMs": { + "initializeChart": 23.1999990940094, + "compileToVGrammar": 2.9999988079071045, + "vRenderDraw": 12.099996089935303, + "doRender": 12.599996089935303 + } + }, + { + "totalMs": 166.60000014305115, + "createMs": 38.799999952316284, + "renderMs": 50.30000019073486, + "releaseMs": 4.1000001430511475, + "updateMs": 63.89999985694885, + "longTaskCount": 1, + "longTaskMs": 157, + "phaseMs": { + "initializeChart": 16.600000143051147, + "compileToVGrammar": 1.7000000476837158, + "vRenderDraw": 12.400001525878906, + "doRender": 12.500001668930054 + } + }, + { + "totalMs": 183.29999995231628, + "createMs": 47.89999985694885, + "renderMs": 55.799999952316284, + "releaseMs": 5.599999904632568, + "updateMs": 66.10000014305115, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 18.999999284744263, + "compileToVGrammar": 2.4000000953674316, + "vRenderDraw": 10.700000047683716, + "doRender": 11.100000381469727 + } + }, + { + "totalMs": 183.40000009536743, + "createMs": 52.60000014305115, + "renderMs": 51.09999990463257, + "releaseMs": 4.099999904632568, + "updateMs": 59.90000009536743, + "longTaskCount": 1, + "longTaskMs": 168, + "phaseMs": { + "initializeChart": 16.199999809265137, + "compileToVGrammar": 2.8000009059906006, + "vRenderDraw": 9.8000009059906, + "doRender": 10.200000762939453 + } + }, + { + "totalMs": 182.89999985694885, + "createMs": 59.799999952316284, + "renderMs": 50.700000047683716, + "releaseMs": 3.8999998569488525, + "updateMs": 58.700000047683716, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 15.700000286102295, + "compileToVGrammar": 3.099998950958252, + "vRenderDraw": 10.299999952316284, + "doRender": 10.499999761581421 + } + }, + { + "totalMs": 200, + "createMs": 68.40000009536743, + "renderMs": 50.39999985694885, + "releaseMs": 4.799999952316284, + "updateMs": 59.10000014305115, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 15.100000381469727, + "compileToVGrammar": 2.8000001907348633, + "vRenderDraw": 12.600000143051147, + "doRender": 12.600000143051147 + } + }, + { + "totalMs": 199.90000009536743, + "createMs": 78.90000009536743, + "renderMs": 48.299999952316284, + "releaseMs": 3.9000000953674316, + "updateMs": 60.299999952316284, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 15.100000619888306, + "compileToVGrammar": 2.0000007152557373, + "vRenderDraw": 9.5, + "doRender": 9.800000429153442 + } + } + ] + } + ] + }, + "fixed-render": { + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36", + "options": { + "samples": 7, + "warmup": 2, + "scenarios": [ + "scatter-10k", + "multi-chart-grid" + ] + }, + "scenarios": [ + { + "name": "scatter-10k", + "samples": [ + { + "totalMs": 50.200000047683716, + "createMs": 0.7999999523162842, + "renderMs": 40, + "releaseMs": 2.700000047683716, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.700000047683716, + "compileToVGrammar": 0.09999990463256836, + "vRenderDraw": 5.700000047683716, + "doRender": 5.799999952316284 + } + }, + { + "totalMs": 49.90000009536743, + "createMs": 0.7999999523162842, + "renderMs": 41.299999952316284, + "releaseMs": 2.8000001907348633, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 3.3000001907348633, + "compileToVGrammar": 0, + "vRenderDraw": 5, + "doRender": 5.099999904632568 + } + }, + { + "totalMs": 50, + "createMs": 0.7999999523162842, + "renderMs": 39, + "releaseMs": 2.5, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.6000001430511475, + "compileToVGrammar": 0, + "vRenderDraw": 5, + "doRender": 5.299999952316284 + } + }, + { + "totalMs": 51.200000047683716, + "createMs": 0.6999998092651367, + "renderMs": 37.700000047683716, + "releaseMs": 2.200000047683716, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.6999998092651367, + "compileToVGrammar": 0, + "vRenderDraw": 4.299999952316284, + "doRender": 4.5 + } + }, + { + "totalMs": 50, + "createMs": 0.7000000476837158, + "renderMs": 35.09999990463257, + "releaseMs": 2, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.200000047683716, + "compileToVGrammar": 0.10000014305114746, + "vRenderDraw": 4.400000095367432, + "doRender": 4.599999904632568 + } + }, + { + "totalMs": 49, + "createMs": 0.5999999046325684, + "renderMs": 36.30000019073486, + "releaseMs": 1.7999999523162842, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 2.1000001430511475, + "compileToVGrammar": 0, + "vRenderDraw": 5.6000001430511475, + "doRender": 5.900000095367432 + } + }, + { + "totalMs": 50, + "createMs": 0.7999999523162842, + "renderMs": 41.299999952316284, + "releaseMs": 2.200000047683716, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 3.299999952316284, + "compileToVGrammar": 0, + "vRenderDraw": 4.3999998569488525, + "doRender": 4.5 + } + } + ] + }, + { + "name": "multi-chart-grid", + "samples": [ + { + "totalMs": 164.90000009536743, + "createMs": 31.299999952316284, + "renderMs": 53.200000047683716, + "releaseMs": 4.299999952316284, + "updateMs": 60.700000047683716, + "longTaskCount": 1, + "longTaskMs": 150, + "phaseMs": { + "initializeChart": 17.399998903274536, + "compileToVGrammar": 2.59999942779541, + "vRenderDraw": 8.899998664855957, + "doRender": 9.19999885559082 + } + }, + { + "totalMs": 167.60000014305115, + "createMs": 39.40000009536743, + "renderMs": 52.799999952316284, + "releaseMs": 4.900000095367432, + "updateMs": 60.59999990463257, + "longTaskCount": 1, + "longTaskMs": 158, + "phaseMs": { + "initializeChart": 19.20000147819519, + "compileToVGrammar": 2.1999998092651367, + "vRenderDraw": 8.699999570846558, + "doRender": 8.999999761581421 + } + }, + { + "totalMs": 167.5, + "createMs": 47.19999980926514, + "renderMs": 50.60000014305115, + "releaseMs": 5.099999904632568, + "updateMs": 57.299999952316284, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 16.499999523162842, + "compileToVGrammar": 2.4000000953674316, + "vRenderDraw": 10.600000619888306, + "doRender": 11.100000619888306 + } + }, + { + "totalMs": 183.20000004768372, + "createMs": 52.200000047683716, + "renderMs": 48.09999990463257, + "releaseMs": 4.6000001430511475, + "updateMs": 64.70000004768372, + "longTaskCount": 1, + "longTaskMs": 170, + "phaseMs": { + "initializeChart": 14.599998474121094, + "compileToVGrammar": 2.6999998092651367, + "vRenderDraw": 11.700000762939453, + "doRender": 12.600000858306885 + } + }, + { + "totalMs": 183.29999995231628, + "createMs": 60.5, + "renderMs": 49.700000047683716, + "releaseMs": 3.8999998569488525, + "updateMs": 57, + "longTaskCount": 1, + "longTaskMs": 171, + "phaseMs": { + "initializeChart": 14.699999332427979, + "compileToVGrammar": 2.200000047683716, + "vRenderDraw": 8.000001430511475, + "doRender": 8.500001668930054 + } + }, + { + "totalMs": 200, + "createMs": 68.80000019073486, + "renderMs": 48.09999990463257, + "releaseMs": 4.099999904632568, + "updateMs": 65.40000009536743, + "longTaskCount": 0, + "longTaskMs": 0, + "phaseMs": { + "initializeChart": 16.19999885559082, + "compileToVGrammar": 2.6000006198883057, + "vRenderDraw": 10.200002193450928, + "doRender": 10.200002193450928 + } + }, + { + "totalMs": 200.09999990463257, + "createMs": 77, + "renderMs": 49, + "releaseMs": 4.3999998569488525, + "updateMs": 55, + "longTaskCount": 1, + "longTaskMs": 185, + "phaseMs": { + "initializeChart": 15.700002193450928, + "compileToVGrammar": 1.2999999523162842, + "vRenderDraw": 8.999998569488525, + "doRender": 9.299998760223389 + } + } + ] + } + ] + }, + "baseline-glyph": { + "fixed": false, + "count": 10000, + "warmup": 2, + "samples": [ + { + "create": { + "ms": 42.200000047683716, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 18.399999856948853, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "basePaint": { + "ms": 14.400000095367432, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "hover": { + "ms": 17.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "clearHover": { + "ms": 13.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "selected": { + "ms": 17.59999990463257, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "ratioChange": { + "ms": 26.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "sameRatio": { + "ms": 24.09999990463257, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,35C50,35,50,25,0,25Z" + }, + "clearSelected": { + "ms": 14.100000143051147, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 14.700000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmClearHover": { + "ms": 14.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "release": { + "ms": 1.2999999523162842, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 43.700000047683716, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 17.90000009536743, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "basePaint": { + "ms": 13.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "hover": { + "ms": 16.40000009536743, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "clearHover": { + "ms": 12.899999856948853, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "selected": { + "ms": 15.700000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "ratioChange": { + "ms": 25.40000009536743, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "sameRatio": { + "ms": 24.59999990463257, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,35C50,35,50,25,0,25Z" + }, + "clearSelected": { + "ms": 14.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 13, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmClearHover": { + "ms": 12.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "release": { + "ms": 0.7000000476837158, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 43, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 17.40000009536743, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "basePaint": { + "ms": 13.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "hover": { + "ms": 14.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "clearHover": { + "ms": 14.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "selected": { + "ms": 15.399999856948853, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "ratioChange": { + "ms": 24.600000143051147, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "sameRatio": { + "ms": 23.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,35C50,35,50,25,0,25Z" + }, + "clearSelected": { + "ms": 12.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 13.400000095367432, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmClearHover": { + "ms": 12.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "release": { + "ms": 0.5999999046325684, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 42.200000047683716, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 15.200000047683716, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "basePaint": { + "ms": 13.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "hover": { + "ms": 16.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "clearHover": { + "ms": 13.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "selected": { + "ms": 15.700000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "ratioChange": { + "ms": 25.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "sameRatio": { + "ms": 24, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,35C50,35,50,25,0,25Z" + }, + "clearSelected": { + "ms": 12.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 14, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmClearHover": { + "ms": 13.399999856948853, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "release": { + "ms": 1, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 45.5, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 19.5, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "basePaint": { + "ms": 14.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "hover": { + "ms": 18, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "clearHover": { + "ms": 12.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "selected": { + "ms": 17, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "ratioChange": { + "ms": 26.199999809265137, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "sameRatio": { + "ms": 24.100000143051147, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,35C50,35,50,25,0,25Z" + }, + "clearSelected": { + "ms": 14.899999856948853, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 15.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmClearHover": { + "ms": 14.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "release": { + "ms": 1, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 42, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 17.699999809265137, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "basePaint": { + "ms": 13.599999904632568, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "hover": { + "ms": 14.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "clearHover": { + "ms": 14.099999904632568, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "selected": { + "ms": 15.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "ratioChange": { + "ms": 24.90000009536743, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "sameRatio": { + "ms": 24.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,35C50,35,50,25,0,25Z" + }, + "clearSelected": { + "ms": 13.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 13.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmClearHover": { + "ms": 13.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "release": { + "ms": 0.6000001430511475, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 46, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 19.100000143051147, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "basePaint": { + "ms": 14.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "hover": { + "ms": 18, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "clearHover": { + "ms": 13.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "selected": { + "ms": 16.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "ratioChange": { + "ms": 25.700000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "sameRatio": { + "ms": 24.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 40000 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,35C50,35,50,25,0,25Z" + }, + "clearSelected": { + "ms": 14.399999856948853, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 14.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "warmClearHover": { + "ms": 14.100000143051147, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 20000 + }, + "release": { + "ms": 1, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + } + ], + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36" + }, + "fixed-glyph": { + "fixed": true, + "count": 10000, + "warmup": 2, + "samples": [ + { + "create": { + "ms": 40, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 23.700000047683716, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "basePaint": { + "ms": 9.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "hover": { + "ms": 21, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "clearHover": { + "ms": 9.300000190734863, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "selected": { + "ms": 46.39999985694885, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 10000, + "writes": 20000 + }, + "ratioChange": { + "ms": 46.60000014305115, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "sameRatio": { + "ms": 10.799999952316284, + "compile": 0, + "resolver": 10000, + "position": 0, + "channel": 0, + "writes": 0 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,27.5C50,27.5,50,17.5,0,17.5Z" + }, + "clearSelected": { + "ms": 60.299999952316284, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 22.40000009536743, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "warmClearHover": { + "ms": 15.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "release": { + "ms": 11.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 39.59999990463257, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 25.700000047683716, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "basePaint": { + "ms": 11.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "hover": { + "ms": 22.700000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "clearHover": { + "ms": 10.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "selected": { + "ms": 51, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 10000, + "writes": 20000 + }, + "ratioChange": { + "ms": 51.10000014305115, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "sameRatio": { + "ms": 12.199999809265137, + "compile": 0, + "resolver": 10000, + "position": 0, + "channel": 0, + "writes": 0 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,27.5C50,27.5,50,17.5,0,17.5Z" + }, + "clearSelected": { + "ms": 47.60000014305115, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 23.399999856948853, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "warmClearHover": { + "ms": 17.100000143051147, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "release": { + "ms": 10.400000095367432, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 39.299999952316284, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 24.5, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "basePaint": { + "ms": 9.300000190734863, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "hover": { + "ms": 22, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "clearHover": { + "ms": 9.599999904632568, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "selected": { + "ms": 46.90000009536743, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 10000, + "writes": 20000 + }, + "ratioChange": { + "ms": 46.09999990463257, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "sameRatio": { + "ms": 10.799999952316284, + "compile": 0, + "resolver": 10000, + "position": 0, + "channel": 0, + "writes": 0 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,27.5C50,27.5,50,17.5,0,17.5Z" + }, + "clearSelected": { + "ms": 42.700000047683716, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 21, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "warmClearHover": { + "ms": 15.099999904632568, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "release": { + "ms": 8.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 45.59999990463257, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 30, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "basePaint": { + "ms": 12.700000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "hover": { + "ms": 24.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "clearHover": { + "ms": 12.600000143051147, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "selected": { + "ms": 52.19999980926514, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 10000, + "writes": 20000 + }, + "ratioChange": { + "ms": 56.200000047683716, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "sameRatio": { + "ms": 11.100000143051147, + "compile": 0, + "resolver": 10000, + "position": 0, + "channel": 0, + "writes": 0 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,27.5C50,27.5,50,17.5,0,17.5Z" + }, + "clearSelected": { + "ms": 47.39999985694885, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 24.199999809265137, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "warmClearHover": { + "ms": 18, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "release": { + "ms": 10.900000095367432, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 54.90000009536743, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 19.700000047683716, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "basePaint": { + "ms": 10.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "hover": { + "ms": 20.399999856948853, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "clearHover": { + "ms": 11.300000190734863, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "selected": { + "ms": 50.5, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 10000, + "writes": 20000 + }, + "ratioChange": { + "ms": 49.299999952316284, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "sameRatio": { + "ms": 12.899999856948853, + "compile": 0, + "resolver": 10000, + "position": 0, + "channel": 0, + "writes": 0 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,27.5C50,27.5,50,17.5,0,17.5Z" + }, + "clearSelected": { + "ms": 49, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 19.59999990463257, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "warmClearHover": { + "ms": 17.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "release": { + "ms": 9.400000095367432, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 39.200000047683716, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 23.799999952316284, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "basePaint": { + "ms": 9.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "hover": { + "ms": 20.59999990463257, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "clearHover": { + "ms": 8.800000190734863, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "selected": { + "ms": 49, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 10000, + "writes": 20000 + }, + "ratioChange": { + "ms": 45.19999980926514, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "sameRatio": { + "ms": 10.800000190734863, + "compile": 0, + "resolver": 10000, + "position": 0, + "channel": 0, + "writes": 0 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,27.5C50,27.5,50,17.5,0,17.5Z" + }, + "clearSelected": { + "ms": 43.5, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 21.299999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "warmClearHover": { + "ms": 15.700000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "release": { + "ms": 8.699999809265137, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + }, + { + "create": { + "ms": 45.59999990463257, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "baseGeometry": { + "ms": 31.5, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "basePaint": { + "ms": 12.799999952316284, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "hover": { + "ms": 24.90000009536743, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "clearHover": { + "ms": 12.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "selected": { + "ms": 53.5, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 10000, + "writes": 20000 + }, + "ratioChange": { + "ms": 54.200000047683716, + "compile": 0, + "resolver": 10000, + "position": 10000, + "channel": 0, + "writes": 10000 + }, + "sameRatio": { + "ms": 10.699999809265137, + "compile": 0, + "resolver": 10000, + "position": 0, + "channel": 0, + "writes": 0 + }, + "example": { + "ratio": 0.75, + "path": "M0,-5C50,-5,50,5,100,5\n L100,27.5C50,27.5,50,17.5,0,17.5Z" + }, + "clearSelected": { + "ms": 48.299999952316284, + "compile": 0, + "resolver": 0, + "position": 10000, + "channel": 0, + "writes": 20000 + }, + "warmHover": { + "ms": 23.700000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "warmClearHover": { + "ms": 18.5, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + }, + "release": { + "ms": 11.200000047683716, + "compile": 0, + "resolver": 0, + "position": 0, + "channel": 0, + "writes": 0 + } + } + ], + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36" + }, + "mouse": { + "baseline": [ + { + "mode": "normal", + "event": "pointerover", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerdown", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "hover", + "selected" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "click", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "hover", + "selected" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerout", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerover", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerdown", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "click", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + } + ], + "core-only": [ + { + "mode": "normal", + "event": "pointerover", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerdown", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "hover", + "selected" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "click", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "hover", + "selected" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + } + ], + "final": [ + { + "mode": "normal", + "event": "pointerover", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerdown", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,56C568,56,568,19,385,19Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "click", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,56C568,56,568,19,385,19Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerout", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,56C568,56,568,19,385,19Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerover", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [ + "hover", + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,56C568,56,568,19,385,19Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerdown", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#B48DEB", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.8695652173913043, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,161C568,161,568,124,385,124Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "click", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#B48DEB", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.8695652173913043, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,161C568,161,568,124,385,124Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerout", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [ + "selected" + ], + "fill": "#B48DEB", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#B48DEB", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.8695652173913043, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,161C568,161,568,124,385,124Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "normal", + "event": "pointerover", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.8695652173913043, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,161C568,161,568,124,385,124Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "normal", + "event": "pointerdown", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "normal", + "event": "click", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + } + ], + "modes": [ + { + "mode": "normal", + "event": "pointerover", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "normal", + "event": "pointerout", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerover", + "item": { + "key": "B-bottom", + "source": "B", + "target": "bottom", + "states": [ + "hover" + ], + "fill": "#B48DEB", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,312C193,312,193,370,375,370\n L375,407C193,407,193,349,10,349Z", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerout", + "item": { + "key": "B-bottom", + "source": "B", + "target": "bottom", + "states": [], + "fill": "#B48DEB", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#B48DEB", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,312C193,312,193,370,375,370\n L375,407C193,407,193,349,10,349Z", + "fill": "#B48DEB", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerover", + "item": { + "key": "C-top", + "source": "C", + "target": "top", + "states": [ + "hover" + ], + "fill": "#009488", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#009488", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,357C193,357,193,216,375,216\n L375,241C193,241,193,382,10,382Z", + "fill": "#009488", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerout", + "item": { + "key": "C-top", + "source": "C", + "target": "top", + "states": [], + "fill": "#009488", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#009488", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,357C193,357,193,216,375,216\n L375,241C193,241,193,382,10,382Z", + "fill": "#009488", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerover", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [ + "hover" + ], + "fill": "#B48DEB", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerout", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [], + "fill": "#B48DEB", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#B48DEB", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#B48DEB", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerover", + "item": { + "key": "A-bottom", + "source": "A", + "target": "bottom", + "states": [ + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,81C193,81,193,333,375,333\n L375,370C193,370,193,118,10,118Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "normal", + "event": "pointerout", + "item": { + "key": "A-bottom", + "source": "A", + "target": "bottom", + "states": [], + "fill": "#1664FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,81C193,81,193,333,375,333\n L375,370C193,370,193,118,10,118Z", + "fill": "#1664FF", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [ + { + "key": "A", + "states": [ + "hover" + ], + "fill": "red" + } + ], + "remainingStrokes": 0 + }, + { + "mode": "normal", + "event": "pointerover", + "item": { + "key": "A", + "states": [ + "hover" + ], + "fill": "red" + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [ + { + "key": "A", + "states": [ + "hover" + ], + "fill": "red" + } + ], + "remainingStrokes": 0 + }, + { + "mode": "normal", + "event": "pointerout", + "item": { + "key": "A", + "states": [], + "fill": "#1664FF" + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "normal", + "event": "pointerover", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "normal", + "event": "pointerdown", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "normal", + "event": "click", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "hover", + "event": "pointerover", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "hover", + "event": "pointerdown", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "hover", + "event": "pointerout", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerover", + "item": { + "key": "B-bottom", + "source": "B", + "target": "bottom", + "states": [ + "selected", + "hover" + ], + "fill": "#B48DEB", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,312C193,312,193,370,375,370\n L375,407C193,407,193,349,10,349Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,312C193,312,193,370,375,370\n L375,407C193,407,193,349,10,349Z", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerout", + "item": { + "key": "B-bottom", + "source": "B", + "target": "bottom", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,312C193,312,193,370,375,370\n L375,407C193,407,193,349,10,349Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerover", + "item": { + "key": "C-middle", + "source": "C", + "target": "middle", + "states": [ + "selected", + "hover" + ], + "fill": "#009488", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,382C193,382,193,299,375,299\n L375,324C193,324,193,407,10,407Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,382C193,382,193,299,375,299\n L375,324C193,324,193,407,10,407Z", + "fill": "#009488", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerout", + "item": { + "key": "C-middle", + "source": "C", + "target": "middle", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,382C193,382,193,299,375,299\n L375,324C193,324,193,407,10,407Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerover", + "item": { + "key": "C-top", + "source": "C", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#009488", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,357C193,357,193,216,375,216\n L375,241C193,241,193,382,10,382Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,357C193,357,193,216,375,216\n L375,241C193,241,193,382,10,382Z", + "fill": "#009488", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerout", + "item": { + "key": "C-top", + "source": "C", + "target": "top", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,357C193,357,193,216,375,216\n L375,241C193,241,193,382,10,382Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.8695652173913043, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,161C568,161,568,124,385,124Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerover", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#B48DEB", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.8695652173913043, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,161C568,161,568,124,385,124Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerout", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [], + "fill": "#B48DEB", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#B48DEB", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,127C193,127,193,44,375,44\n L375,216C193,216,193,299,10,299Z", + "fill": "#B48DEB", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "hover", + "event": "pointerover", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "hover", + "event": "pointerout", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,56C568,56,568,19,385,19Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerover", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,56C568,56,568,19,385,19Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "click", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,56C568,56,568,19,385,19Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "hover", + "event": "pointerout", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [], + "fill": "#1664FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "hover", + "event": "pointerover", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "hover", + "event": "pointerdown", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "hover", + "event": "click", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [], + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "vertical", + "event": "pointerover", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,215C1,313,69,313,69,410\n L331,410C331,313,263,313,263,215Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "vertical", + "event": "pointerout", + "item": null, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,215C1,313,69,313,69,410\n L331,410C331,313,263,313,263,215Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "vertical", + "event": "pointerover", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,10C1,108,1,108,1,205\n L81,205C81,108,81,108,81,10Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,215C1,313,69,313,69,410\n L331,410C331,313,263,313,263,215Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "vertical", + "event": "pointerdown", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M1,10C1,108,1,108,1,205\n L81,205C81,108,81,108,81,10Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,10C1,108,1,108,1,205\n L81,205C81,108,81,108,81,10Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M1,215C1,313,69,313,69,410\n L331,410C331,313,263,313,263,215Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,215C1,313,69,313,69,410\n L103,410C103,313,35,313,35,215Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "vertical", + "event": "click", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M1,10C1,108,1,108,1,205\n L81,205C81,108,81,108,81,10Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,10C1,108,1,108,1,205\n L81,205C81,108,81,108,81,10Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M1,215C1,313,69,313,69,410\n L331,410C331,313,263,313,263,215Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,215C1,313,69,313,69,410\n L103,410C103,313,35,313,35,215Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "vertical", + "event": "pointerout", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M1,10C1,108,1,108,1,205\n L81,205C81,108,81,108,81,10Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,10C1,108,1,108,1,205\n L81,205C81,108,81,108,81,10Z", + "fill": "#1664FF", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M1,215C1,313,69,313,69,410\n L331,410C331,313,263,313,263,215Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,215C1,313,69,313,69,410\n L103,410C103,313,35,313,35,215Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "vertical", + "event": "pointerover", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [ + "hover", + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M226,10C226,108,81,108,81,205\n L400,205C400,108,545,108,545,10Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.13043478260869565, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M1,215C1,313,69,313,69,410\n L331,410C331,313,263,313,263,215Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,215C1,313,69,313,69,410\n L103,410C103,313,35,313,35,215Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "vertical", + "event": "pointerdown", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#B48DEB", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M226,10C226,108,81,108,81,205\n L400,205C400,108,545,108,545,10Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M226,10C226,108,81,108,81,205\n L400,205C400,108,545,108,545,10Z", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.8695652173913043, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M1,215C1,313,69,313,69,410\n L331,410C331,313,263,313,263,215Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,215C1,313,69,313,69,410\n L297,410C297,313,229,313,229,215Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "vertical", + "event": "click", + "item": { + "key": "B-top", + "source": "B", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#B48DEB", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M226,10C226,108,81,108,81,205\n L400,205C400,108,545,108,545,10Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M226,10C226,108,81,108,81,205\n L400,205C400,108,545,108,545,10Z", + "fill": "#B48DEB", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "selected" + ], + "fill": "#1AC6FF", + "opacity": 0.15, + "ratio": 0.8695652173913043, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M1,215C1,313,69,313,69,410\n L331,410C331,313,263,313,263,215Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M1,215C1,313,69,313,69,410\n L297,410C297,313,229,313,229,215Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "adjacency", + "event": "pointerover", + "item": { + "key": "top", + "fill": "#1AC6FF" + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 0 + }, + { + "mode": "adjacency", + "event": "pointerout", + "item": { + "key": "top", + "fill": "#1AC6FF" + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "adjacency", + "event": "pointerover", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "fill": "#1AC6FF", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#1AC6FF", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "adjacency", + "event": "pointerdown", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + }, + { + "mode": "adjacency", + "event": "click", + "item": { + "key": "A-top", + "source": "A", + "target": "top", + "states": [ + "selected", + "hover" + ], + "fill": "#1664FF", + "opacity": 0.15, + "ratio": 1, + "backgroundStyle": { + "fill": "#e8e8e8" + }, + "sub": [ + { + "name": "back", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#e8e8e8", + "stroke": "#000000", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M10,1C193,1,193,1,375,1\n L375,44C193,44,193,44,10,44Z", + "fill": "#1664FF", + "stroke": "#000000", + "fillOpacity": 0.15 + } + ] + }, + "target": { + "key": "top-00", + "source": "top", + "target": "00", + "states": [ + "blur" + ], + "fill": "#e8e8e8", + "opacity": 0.15, + "sub": [ + { + "name": "back", + "path": "", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + }, + { + "name": "front", + "path": "M385,1C568,1,568,38,750,38\n L750,180C568,180,568,143,385,143Z", + "fill": "#e8e8e8", + "fillOpacity": 0.15 + } + ] + }, + "hoverNodes": [], + "remainingStrokes": 1 + } + ] + } +} diff --git a/packages/vchart/__tests__/unit/mark/glyph-state.test.ts b/packages/vchart/__tests__/unit/mark/glyph-state.test.ts new file mode 100644 index 0000000000..0dbc92feeb --- /dev/null +++ b/packages/vchart/__tests__/unit/mark/glyph-state.test.ts @@ -0,0 +1,201 @@ +import { createGroup } from '@visactor/vrender-core'; +import { LinkPathMark, registerLinkPathMark } from '../../../src/mark/link-path'; +import { BoxPlotMark, registerBoxPlotMark } from '../../../src/mark/box-plot'; +import { RippleMark, registerRippleMark } from '../../../src/mark/ripple'; +import { LiquidMark, registerLiquidMark } from '../../../src/mark/liquid'; +import { addRuntimeState } from '../../../src/mark/utils/glyph'; +import { markContext } from '../../util/context'; +import { ribbonThickness } from '../series/fixtures/sankey-related'; + +const createGraphic = (mark: any, attrs: any, datum = {}) => { + const g = mark._createGraphic(attrs); + g.context = { data: [datum], states: [] }; + mark._afterCreateGraphic(g); + return g; +}; +const linkAttrs = { x0: 0, y0: 10, x1: 100, y1: 20, thickness: 20, round: false, fill: 'red' }; + +beforeAll(() => { + registerLinkPathMark(); + registerBoxPlotMark(); + registerRippleMark(); + registerLiquidMark(); +}); + +test('shared runtime-only selected state resolves each Glyph independently and refreshes without clearing', () => { + const mark: any = new LinkPathMark('link', markContext); + mark.created(); + mark._product = createGroup({}); + mark._encoderOfState = {}; + mark._applySharedStateDefinitions(); + const definition = mark._product.sharedStateDefinitions; + const a = createGraphic(mark, linkAttrs); + const b = createGraphic(mark, linkAttrs); + mark._product.appendChild(a); + mark._product.appendChild(b); + const encoder = jest.spyOn(mark, '_positionEncoder'); + addRuntimeState(a, 'selected', { ratio: 0.25 }, true, false); + addRuntimeState(b, 'selected', { ratio: 0.75 }, true, false); + expect(ribbonThickness(a.getSubGraphic()[1].attribute.path)).toEqual([5, 5]); + expect(ribbonThickness(b.getSubGraphic()[1].attribute.path)).toEqual([15, 15]); + const write = jest.spyOn(a, 'commitSubGraphicAttributes'); + encoder.mockClear(); + addRuntimeState(a, 'selected', { ratio: 0.5 }, true, false); + expect(ribbonThickness(a.getSubGraphic()[1].attribute.path)).toEqual([10, 10]); + expect(encoder).toHaveBeenCalledTimes(1); + expect(write).toHaveBeenCalledTimes(1); + expect(mark._product.sharedStateDefinitions).toBe(definition); + encoder.mockClear(); + write.mockClear(); + addRuntimeState(a, 'selected', { ratio: 0.5 }, true, false); + expect(encoder).not.toHaveBeenCalled(); + expect(write).not.toHaveBeenCalled(); + expect(b.attribute.ratio).toBe(0.75); +}); + +test('static, data-driven and runtime state attributes compose in the shared resolver', () => { + const mark: any = new LinkPathMark('link', markContext); + mark.created(); + mark.setAttribute('backgroundStyle', { fill: 'gray' }, 'selected'); + mark.setAttribute('stroke', (datum: any) => datum.stroke, 'selected'); + mark.setAttribute('ratio', 0.1, 'selected'); + mark._updateEncoderByState(); + mark._product = createGroup({}); + mark._applySharedStateDefinitions(); + const g = createGraphic(mark, linkAttrs, { stroke: 'blue' }); + mark._product.appendChild(g); + addRuntimeState(g, 'selected', { ratio: 0.4 }, true, false); + expect(g.attribute).toMatchObject({ ratio: 0.4, stroke: 'blue', backgroundStyle: { fill: 'gray' } }); + expect(ribbonThickness(g.getSubGraphic()[1].attribute.path)).toEqual([8, 8]); + expect(g.getSubGraphic()[0].attribute.fill).toBe('gray'); +}); + +test('channel removal restores current inheritance, and paint changes do not encode geometry', () => { + const mark: any = new LinkPathMark('link', markContext); + const g = createGraphic(mark, linkAttrs); + g.states = { + hover: { stroke: 'black' }, + selected: { backgroundStyle: { fill: 'gray', stroke: 'purple' }, ratio: 0.5 } + }; + g.setStates(['hover', 'selected'], false); + const [back, front] = g.getSubGraphic(); + expect(back.attribute.stroke).toBe('purple'); + expect(front.attribute.stroke).toBe('black'); + g.states = { selected: { backgroundStyle: { fill: 'gray' }, ratio: 0.5 } }; + g.setStates(['selected'], { animate: false }); + expect(back.attribute.stroke).toBeUndefined(); + expect(front.attribute.stroke).toBeUndefined(); + const encode = jest.spyOn(mark, '_positionEncoder'); + g.setAttribute('fill', 'blue'); + expect(encode).not.toHaveBeenCalled(); + g.clearStates(false); + expect(back.attribute.fill).toBe('blue'); + expect(Object.prototype.hasOwnProperty.call(back.attribute, 'fill')).toBe(false); + expect(back.attribute.path).toBe(''); + expect(ribbonThickness(front.attribute.path)).toEqual([20, 20]); +}); + +test.each(['line', 'bar'])('boxPlot clears conditional angle and anchor outputs (%s)', shaftShape => { + const mark: any = new BoxPlotMark('box', markContext); + mark.setGlyphConfig({ direction: 'horizontal', shaftShape }); + const g = createGraphic(mark, { + x: 10, + y: 20, + min: 0, + q1: 5, + median: 10, + q3: 15, + max: 20, + boxHeight: 10, + ruleHeight: 10, + minMaxHeight: 2, + q1q3Height: 10 + }); + g.states = { hover: { angle: 0.5 } }; + g.setStates(['hover'], false); + g.getSubGraphic().forEach((child: any) => expect(child.attribute.angle).toBe(0.5)); + g.clearStates(false); + g.getSubGraphic().forEach((child: any) => { + expect(child.attribute.angle).toBeUndefined(); + expect(child.attribute.anchor).toBeUndefined(); + expect(Object.prototype.hasOwnProperty.call(child.attribute, 'angle')).toBe(false); + expect(Object.prototype.hasOwnProperty.call(child.attribute, 'anchor')).toBe(false); + }); +}); + +test('ripple retains derived opacity and restores geometry', () => { + const mark: any = new RippleMark('ripple', markContext); + const g = createGraphic(mark, { size: 20, ripple: 0, fill: 'red' }); + g.states = { hover: { ripple: 1, fillOpacity: 1 } }; + g.setStates(['hover'], false); + expect(g.getSubGraphic().map((child: any) => child.attribute.fillOpacity)).toEqual([0.5, 0.25, 0]); + expect(g.getSubGraphic().map((child: any) => child.attribute.size)).toEqual([30, 40, 50]); + g.clearStates(false); + expect(g.getSubGraphic().map((child: any) => child.attribute.fillOpacity)).toEqual([0.75, 0.5, 0.25]); + expect(g.getSubGraphic().map((child: any) => child.attribute.size)).toEqual([20, 30, 40]); +}); + +test('liquid retains child opacity and does not rebuild points for a paint-only update', () => { + const mark: any = new LiquidMark('liquid', markContext); + const g = createGraphic(mark, { y: 20, height: 100, wave: 0, fill: 'red' }); + const points = g.getSubGraphic().map((child: any) => child.attribute.points); + const encode = jest.spyOn(mark, '_positionEncoder'); + g.states = { hover: { fill: 'blue' } }; + g.setStates(['hover'], false); + expect(encode).not.toHaveBeenCalled(); + g.getSubGraphic().forEach((child: any, i: number) => { + expect(child.attribute.points).toBe(points[i]); + expect(child.attribute.fill).toBe('blue'); + }); + expect(g.getSubGraphic().map((child: any) => child.attribute.fillOpacity)).toEqual([1, 0.66, 0.33]); + g.setAttribute('wave', 0.5); + expect(encode).toHaveBeenCalledTimes(1); + expect(g.getSubGraphic()[0].attribute.points[0].x).toBe(-450); +}); + +test.each([0, 1])('ratio endpoint %s has the correct foreground and full background thickness', ratio => { + const mark: any = new LinkPathMark('link', markContext); + const g = createGraphic(mark, linkAttrs); + g.states = { selected: { ratio } }; + g.setStates(['selected'], false); + const [back, front] = g.getSubGraphic(); + expect(ribbonThickness(front.attribute.path)).toEqual([20 * ratio, 20 * ratio]); + expect(ribbonThickness(back.attribute.path)).toEqual([20, 20]); + g.clearStates(false); + expect(ribbonThickness(front.attribute.path)).toEqual([20, 20]); + expect(back.attribute.path).toBe(''); +}); + +test('reused Glyph discards the previous datum runtime ratio', () => { + const mark: any = new LinkPathMark('link', markContext); + mark.created(); + mark._product = createGroup({}); + mark._encoderOfState = {}; + mark._applySharedStateDefinitions(); + const g = createGraphic(mark, linkAttrs); + mark._product.appendChild(g); + addRuntimeState(g, 'selected', { ratio: 0.25 }, true, false); + g.context = { data: [{ key: 'next' }], states: [], reusing: true, finalAttrs: { ...linkAttrs, thickness: 40 } }; + mark._graphics = [g]; + mark._runApplyGraphic([g]); + expect(g.runtimeStateCache).toBeUndefined(); + expect(g.attribute.ratio).toBeUndefined(); + addRuntimeState(g, 'selected', {}, true, false); + expect(g.attribute.ratio).toBeUndefined(); + expect(ribbonThickness(g.getSubGraphic()[1].attribute.path)).toEqual([40, 40]); +}); + +test('progressive creation initializes Glyph children after datum and context are ready', () => { + const mark: any = new LinkPathMark('link', markContext); + mark.created(); + mark._product = createGroup({}); + const progressiveGroup = createGroup({}); + mark._product.appendChild(progressiveGroup); + mark.renderContext = { progressive: {} }; + mark._graphics = [{ context: { data: [{}], states: [], finalAttrs: linkAttrs } }]; + mark._runApplyGraphic(mark._graphics); + const g = mark._graphics[0]; + expect(g.parent).toBe(progressiveGroup); + expect(g.getSubGraphic()[0].attribute.path).toBe(''); + expect(ribbonThickness(g.getSubGraphic()[1].attribute.path)).toEqual([20, 20]); +}); diff --git a/packages/vchart/__tests__/unit/series/fixtures/sankey-related.ts b/packages/vchart/__tests__/unit/series/fixtures/sankey-related.ts new file mode 100644 index 0000000000..d3a097cab7 --- /dev/null +++ b/packages/vchart/__tests__/unit/series/fixtures/sankey-related.ts @@ -0,0 +1,51 @@ +export const createRelatedSpec = () => ({ + type: 'sankey', + width: 800, + height: 460, + data: [ + { + id: 'data', + values: [ + { + nodes: [ + { name: 'A', children: [{ name: 'top', children: [{ name: '00', value: 15 }] }] }, + { name: 'B', children: [{ name: 'top', children: [{ name: '00', value: 100 }] }] }, + { name: 'C', children: [{ name: 'bottom', value: 30 }] } + ] + } + ] + } + ], + categoryField: 'name', + valueField: 'value', + nodeKey: (datum: any) => datum.name, + nodeAlign: 'left', + nodeWidth: 10, + nodeGap: 8, + node: { state: { hover: { fill: 'red' }, blur: { fillOpacity: 0.15 } } }, + link: { + style: { round: false }, + state: { + hover: { stroke: '#000000' }, + selected: { backgroundStyle: { fill: '#e8e8e8' } }, + blur: { fill: '#e8e8e8' } + } + }, + emphasis: { enable: true, effect: 'related' } +}); + +/** Read ribbon endpoints independently of the production path encoder (no arrows). */ +export const ribbonThickness = (path: string, vertical = false) => { + const commands = path.match(/[MCLZ][^MCLZ]*/g); + expect(commands.map(command => command[0])).toEqual(['M', 'C', 'L', 'C', 'Z']); + const endpoints = commands.slice(0, 4).map(command => { + const values = command + .slice(1) + .trim() + .split(/[\s,]+/) + .map(Number); + return values.slice(-2); + }); + const axis = vertical ? 0 : 1; + return [Math.abs(endpoints[3][axis] - endpoints[0][axis]), Math.abs(endpoints[2][axis] - endpoints[1][axis])]; +}; diff --git a/packages/vchart/__tests__/unit/series/sankey-emphasis-state.test.ts b/packages/vchart/__tests__/unit/series/sankey-emphasis-state.test.ts index 27f37ae688..8fc645b467 100644 --- a/packages/vchart/__tests__/unit/series/sankey-emphasis-state.test.ts +++ b/packages/vchart/__tests__/unit/series/sankey-emphasis-state.test.ts @@ -4,6 +4,7 @@ import VChart from '../../../src'; import { Event_Bubble_Level } from '../../../src/constant/event'; import { findMarkGraphic } from '../../../src/util/mark'; import { createDiv, removeDom } from '../../util/dom'; +import { createRelatedSpec, ribbonThickness } from './fixtures/sankey-related'; const createSpec = () => ({ type: 'sankey', @@ -119,4 +120,81 @@ describe('sankey emphasis state', () => { expect(other.getSubGraphic()[0].attribute.fillOpacity).toBe(0.05); expect(other.getSubGraphic()[1].attribute.fillOpacity).toBe(0.05); }); + + test.each([false, true])( + 'related selection changes actual ribbon thickness and restores it (vertical=%s)', + vertical => { + chart = new VChart( + { ...createRelatedSpec(), direction: vertical ? 'vertical' : 'horizontal' }, + { dom, animation: false } + ); + chart.renderSync(); + const series = chart.getChart().getAllSeries()[0] as any; + const linkMark = series._linkMark; + const links = linkMark.getGraphics(); + const findLink = (source: string, target: string) => + links.find(g => { + const datum = g.context.data[0]; + return datum.source === source && datum.target === target; + }); + const target = findLink('top', '00'); + const front = target.getSubGraphic().find(g => g.name === 'front'); + const back = target.getSubGraphic().find(g => g.name === 'back'); + const initialPath = front.attribute.path; + const click = (g?: any) => { + const item = g && findMarkGraphic(chart.getCompiler().getRootGroup(), g.getSubGraphic()[0]); + series.event.emit('pointerdown', { item, mark: item && linkMark }, Event_Bubble_Level.chart); + }; + for (const [source, ratio] of [ + ['A', 15 / 115], + ['B', 100 / 115], + ['A', 15 / 115] + ]) { + click(findLink(source, 'top')); + expect(target.attribute.ratio).toBeCloseTo(ratio); + for (const thickness of ribbonThickness(front.attribute.path, vertical)) { + expect(thickness).toBeCloseTo(target.attribute.thickness * ratio); + } + for (const thickness of ribbonThickness(back.attribute.path, vertical)) { + expect(thickness).toBeCloseTo(target.attribute.thickness); + } + expect(back.attribute.path).toBe(initialPath); + expect(back.attribute.fill).toBe('#e8e8e8'); + } + click(); + expect(target.currentStates).toEqual([]); + expect(target.attribute.ratio).toBeUndefined(); + expect(target.attribute.backgroundStyle).toBeUndefined(); + expect(front.attribute.path).toBe(initialPath); + expect(back.attribute.path).toBe(''); + } + ); + + test.each(['data', 'spec'])('re-encodes related selection after a %s update', update => { + chart = new VChart(createRelatedSpec(), { dom, animation: false }); + chart.renderSync(); + const selectA = () => { + const series = chart.getChart().getAllSeries()[0] as any; + const mark = series._linkMark; + const links = mark.getGraphics(); + const a = links.find(g => g.context.data[0].source === 'A' && g.context.data[0].target === 'top'); + series.event.emit('pointerdown', { item: a, mark }, Event_Bubble_Level.chart); + return links.find(g => g.context.data[0].source === 'top' && g.context.data[0].target === '00'); + }; + expect(selectA().attribute.ratio).toBeCloseTo(15 / 115); + const next = createRelatedSpec(); + next.data[0].values[0].nodes[0].children[0].children[0].value = 30; + if (update === 'data') { + chart.updateDataSync('data', next.data[0].values); + } else { + next.width = 900; + chart.updateSpecSync(next); + } + const target = selectA(); + expect(target.attribute.ratio).toBeCloseTo(30 / 130); + const front = target.getSubGraphic().find(g => g.name === 'front'); + for (const thickness of ribbonThickness(front.attribute.path)) { + expect(thickness).toBeCloseTo((target.attribute.thickness * 30) / 130); + } + }); }); diff --git a/packages/vchart/src/mark/base/base-mark.ts b/packages/vchart/src/mark/base/base-mark.ts index c18af7e291..6081f0624f 100644 --- a/packages/vchart/src/mark/base/base-mark.ts +++ b/packages/vchart/src/mark/base/base-mark.ts @@ -1173,6 +1173,14 @@ export class BaseMark extends GrammarItem implements IMar return Factory.createGraphicComponent(this.type, attrs); } + protected _afterCreateGraphic(g: IMarkGraphic): void { + // Subclasses may initialize derived graphics after datum/context are available. + } + + protected _getRuntimeStateKeys(): Record | undefined { + return undefined; + } + protected _runGroupData(data: Datum[]) { this._keyGetter = isFunction(this.key) ? (this.key as (datum: Datum) => string) @@ -1640,9 +1648,17 @@ export class BaseMark extends GrammarItem implements IMar return; } + const runtimeStateKeys = this._getRuntimeStateKeys(); const stateNames = Object.keys(this._encoderOfState ?? {}).filter( stateName => stateName !== 'group' && stateName !== 'update' ); + if (runtimeStateKeys) { + Object.keys(runtimeStateKeys).forEach(name => { + if (!stateNames.includes(name)) { + stateNames.push(name); + } + }); + } if (!stateNames.length) { if (this._product.sharedStateDefinitions !== undefined) { @@ -1662,7 +1678,8 @@ export class BaseMark extends GrammarItem implements IMar const cacheKeys: string[] = []; stateNames.forEach(stateName => { - const encoder = this._encoderOfState[stateName]; + const encoder = this._encoderOfState?.[stateName]; + const runtimeKeys = runtimeStateKeys?.[stateName]; const patch: Record = {}; const dynamicEncoder: Record any> = {}; const patchKeys: string[] = []; @@ -1678,7 +1695,7 @@ export class BaseMark extends GrammarItem implements IMar } }); - if (!patchKeys.length && !dynamicKeys.length) { + if (!patchKeys.length && !dynamicKeys.length && !runtimeKeys?.length) { return; } @@ -1690,7 +1707,13 @@ export class BaseMark extends GrammarItem implements IMar definition.patch = patch; } - if (dynamicKeys.length) { + if (runtimeKeys?.length) { + definition.declaredAffectedKeys = Array.from(new Set([...dynamicKeys, ...runtimeKeys])); + definition.resolver = ({ graphic }: StateResolveContext>) => { + const attrs = dynamicKeys.length ? this._runEncoderOfGraphic(dynamicEncoder, graphic as IMarkGraphic) : {}; + return Object.assign(attrs, (graphic as IMarkGraphic).runtimeStateCache?.[stateName]); + }; + } else if (dynamicKeys.length) { definition.declaredAffectedKeys = dynamicKeys; definition.resolver = ({ graphic }: StateResolveContext>) => this._runEncoderOfGraphic(dynamicEncoder, graphic as IMarkGraphic); @@ -1701,6 +1724,7 @@ export class BaseMark extends GrammarItem implements IMar [ stateName, definition.priority, + runtimeKeys?.join(','), patchKeys.sort().map(key => `${key}:${this._getSharedStateDefinitionValueKey(patch[key])}`), dynamicKeys.sort().map(key => { const stateStyle = this.stateStyle[stateName]?.[key]; @@ -1807,6 +1831,7 @@ export class BaseMark extends GrammarItem implements IMar g.context = mockGraphic.context; g.context.diffAttrs = finalAttrs; g.stateSort = this._stateSort; + this._afterCreateGraphic(g); const gIndex = this._graphics === graphics ? index : index + this._graphics.length - graphics.length; if (gIndex >= 0) { @@ -1827,6 +1852,7 @@ export class BaseMark extends GrammarItem implements IMar const diffAttrs = this._excludeStateControlledDiffAttrs(g, getDiffAttributesOfGraphic(g, finalAttrs)); g.context.diffAttrs = diffAttrs; if (g.context.reusing) { + g.runtimeStateCache = undefined; // 表示正在被复用,需要重设属性的 // TODO 理论上复用后只会走一次enter,所以这里lastAttrs不需要后续清除,这里需要硬拷贝(通过initAttributes重设属性也行) g.context.lastAttrs = g.attribute; diff --git a/packages/vchart/src/mark/box-plot.ts b/packages/vchart/src/mark/box-plot.ts index 9ccc218acf..df237f25ff 100644 --- a/packages/vchart/src/mark/box-plot.ts +++ b/packages/vchart/src/mark/box-plot.ts @@ -21,7 +21,8 @@ const BAR_BOX_PLOT_CHANNELS = [ 'min', 'max', 'median', - 'angle' + 'angle', + 'anchor' ]; const BOX_PLOT_CHANNELS = [ @@ -36,7 +37,8 @@ const BOX_PLOT_CHANNELS = [ 'min', 'max', 'median', - 'angle' + 'angle', + 'anchor' ]; export class BoxPlotMark diff --git a/packages/vchart/src/mark/glyph.ts b/packages/vchart/src/mark/glyph.ts index 6e33a44368..75bddde90c 100644 --- a/packages/vchart/src/mark/glyph.ts +++ b/packages/vchart/src/mark/glyph.ts @@ -1,36 +1,30 @@ import type { ICommonSpec } from '../typings/visual'; import { BaseMark } from './base/base-mark'; -import type { IGlyph, IGlyphGraphicAttribute, IGraphic } from '@visactor/vrender-core'; +import type { IGlyph, IGlyphGraphicAttribute, IGraphic, ISetAttributeContext } from '@visactor/vrender-core'; import { createGlyph, registerGlyph, registerShadowRoot } from '../vrender-bridge'; import type { IGlyphMark } from './interface/mark'; import type { MarkType } from './interface/type'; import { Factory } from '../core/factory'; import type { Datum } from '../typings/common'; import type { IMarkGraphic } from './interface/common'; -import { DiffState } from './interface/enum'; -import { merge } from '@visactor/vutils'; - -const GLYPH_INHERITED_STYLE_ATTRIBUTES = new Set([ - 'fill', - 'fillOpacity', - 'stroke', - 'strokeOpacity', - 'opacity', - 'lineWidth', - 'lineDash', - 'lineDashOffset', - 'lineCap', - 'lineJoin', - 'miterLimit', - 'shadowBlur', - 'shadowColor', - 'shadowOffsetX', - 'shadowOffsetY', - 'visible', - 'pickable', - 'cursor' -]); -const GLYPH_STATE_ATTRIBUTE_UPDATE_TYPE = 2; + +type SubGraphicAttributes = Record>; + +type GlyphEncoderDefinition = { + positionChannels: string[]; + channelEncoder: GlyphMark['_channelEncoder']; + subMarks: GlyphMark['_subMarks']; + positionKeys: string[]; + channelKeys: string[]; +}; + +type GlyphEncoding = { + definition: GlyphEncoderDefinition; + children: IGraphic[]; + data?: Datum[]; + inputs: any[]; + parts: SubGraphicAttributes[]; +}; export abstract class GlyphMark extends BaseMark @@ -54,6 +48,7 @@ export abstract class GlyphMark setGlyphConfig(cfg: Cfg) { this._glyphConfig = cfg; + this._glyphEncoderDefinition = undefined; } getGlyphConfig() { @@ -70,117 +65,163 @@ export abstract class GlyphMark protected _channelEncoder: Record Record>; - private _getInheritedStyleAttrs(attributes: any) { - let inheritedStyleAttrs: any = null; - - Object.keys(attributes).forEach(channel => { - if (GLYPH_INHERITED_STYLE_ATTRIBUTES.has(channel)) { - inheritedStyleAttrs = inheritedStyleAttrs ?? {}; - inheritedStyleAttrs[channel] = attributes[channel]; - } - }); - - return inheritedStyleAttrs; - } - - private _getInheritedStyleAttrsMap(glyph: IGlyph, attributes: any) { - const inheritedStyleAttrs = this._getInheritedStyleAttrs(attributes); - - if (!inheritedStyleAttrs) { - return null; + private _glyphEncoderDefinition?: GlyphEncoderDefinition; + private _glyphEncodings = new WeakMap(); + + private _getGlyphEncoderDefinition(): GlyphEncoderDefinition { + let definition = this._glyphEncoderDefinition; + if ( + !definition || + definition.positionChannels !== this._positionChannels || + definition.channelEncoder !== this._channelEncoder || + definition.subMarks !== this._subMarks + ) { + definition = { + positionChannels: this._positionChannels, + channelEncoder: this._channelEncoder, + subMarks: this._subMarks, + positionKeys: Array.from(new Set(this._positionChannels ?? [])), + channelKeys: Object.keys(this._channelEncoder ?? {}) + }; + this._glyphEncoderDefinition = definition; } - - return glyph.getSubGraphic().reduce((map, subGraphic) => { - if (subGraphic) { - map[subGraphic.name] = { ...inheritedStyleAttrs }; - } - return map; - }, {} as Record); + return definition; } - private _syncInheritedStyleAttrs(glyph: IGlyph, attributes: any) { - const inheritedStyleAttrs = this._getInheritedStyleAttrs(attributes); - - if (!inheritedStyleAttrs) { - return; - } - - glyph.getSubGraphic().forEach(subGraphic => { - subGraphic?.setAttributes({ ...inheritedStyleAttrs }); - }); + protected _afterCreateGraphic(g: IMarkGraphic & IGlyph): void { + g.setSubGraphicEncoder(this._encodeGlyph); } - private _onGlyphAttributeUpdate(glyph: IGlyph) { - return (newAttributes: any) => { - const positionChannels = this.getPositionChannels(); - let subAttrsMap = - positionChannels && this._positionEncoder && Object.keys(newAttributes).some(k => positionChannels.includes(k)) - ? this._positionEncoder(newAttributes, glyph?.context?.data?.[0], glyph) - : null; - - if (this._channelEncoder) { - Object.keys(this._channelEncoder).forEach(channel => { - if (channel in newAttributes) { - const channelAttrsMap = this._channelEncoder[channel](newAttributes[channel]); - - subAttrsMap = subAttrsMap ? merge(subAttrsMap, channelAttrsMap) : channelAttrsMap; - } - }); + private _encodeGlyph = (glyph: IGlyph, context?: ISetAttributeContext): void => { + const definition = this._getGlyphEncoderDefinition(); + const children = glyph.getSubGraphic(); + const previous = this._glyphEncodings.get(glyph); + const reset = !previous || previous.definition !== definition || previous.children !== children; + const cache: GlyphEncoding = reset ? { definition, children, inputs: [], parts: [] } : previous; + const data = glyph.context.data; + const force = reset || cache.data !== data || glyph.context.reusing; + let dirty: Map> | undefined; + const collectKey = (name: string, key: string) => { + dirty ??= new Map(); + let keys = dirty.get(name); + if (!keys) { + keys = new Set(); + dirty.set(name, keys); } - - const inheritedStyleAttrsMap = this._getInheritedStyleAttrsMap(glyph, newAttributes); - if (inheritedStyleAttrsMap) { - subAttrsMap = subAttrsMap ? merge(inheritedStyleAttrsMap, subAttrsMap) : inheritedStyleAttrsMap; + keys.add(key); + }; + const collectKeys = (part?: SubGraphicAttributes) => { + if (part) { + for (const name of Object.keys(part)) { + for (const key of Object.keys(part[name] ?? {})) { + collectKey(name, key); + } + } } - - if (subAttrsMap) { - glyph.getSubGraphic().forEach(subGraphic => { - if (subGraphic && subAttrsMap[subGraphic.name]) { - subGraphic.setAttributes(subAttrsMap[subGraphic.name]); + }; + if (reset && previous) { + previous.parts.forEach(collectKeys); + } + const updatePart = (index: number, part?: SubGraphicAttributes) => { + const previousPart = cache.parts[index]; + if (force) { + collectKeys(previousPart); + collectKeys(part); + } else { + if (previousPart) { + for (const name of Object.keys(previousPart)) { + const next = part?.[name]; + for (const key of Object.keys(previousPart[name] ?? {})) { + if (!next || !Object.prototype.hasOwnProperty.call(next, key)) { + collectKey(name, key); + } + } } - }); + } + if (part) { + for (const name of Object.keys(part)) { + const prev = previousPart?.[name]; + const next = part[name]; + for (const key of Object.keys(next ?? {})) { + if (!prev || !Object.prototype.hasOwnProperty.call(prev, key) || prev[key] !== next[key]) { + collectKey(name, key); + } + } + } + } } - - return newAttributes; + cache.parts[index] = part; }; - } - - protected _setStateOfGraphic = (g: IMarkGraphic, hasAnimation?: boolean) => { - g.clearStates(); - - if (g.context.diffState === DiffState.enter || g.context.diffState === DiffState.update) { - g.glyphStateProxy = (stateName: string, nexStates: string[]) => { - const glyphAttrs = { - attributes: { - ...this._runEncoderOfGraphic(this._encoderOfState?.[stateName], g), - ...(g.runtimeStateCache ? g.runtimeStateCache[stateName] : null) + let positionChanged = force; + const { positionKeys, channelKeys } = definition; + for (let i = 0; i < positionKeys.length; i++) { + const value = (glyph.attribute as any)[positionKeys[i]]; + positionChanged = positionChanged || cache.inputs[i] !== value; + cache.inputs[i] = value; + } + if (positionChanged) { + updatePart(0, this._positionEncoder?.(glyph.attribute, data[0], glyph)); + } + for (let i = 0; i < channelKeys.length; i++) { + const channel = channelKeys[i]; + const value = (glyph.attribute as any)[channel]; + const inputIndex = positionKeys.length + i; + if (force || cache.inputs[inputIndex] !== value) { + updatePart(i + 1, value === undefined ? undefined : this._channelEncoder[channel](value)); + } + cache.inputs[inputIndex] = value; + } + cache.data = data; + if (reset) { + this._glyphEncodings.set(glyph, cache); + } + if (!dirty) { + return; + } + for (const child of children) { + const keys = dirty.get(child.name); + if (!keys) { + continue; + } + let patch: Record | undefined; + let removedKeys: string[] | undefined; + let changed = false; + const base = child.baseAttributes; + const defaults = definition.subMarks[child.name]?.defaultAttributes; + keys.forEach(key => { + let hasValue = false; + let value: any; + for (let i = cache.parts.length - 1; i >= 0; i--) { + const attrs = cache.parts[i]?.[child.name]; + if (attrs && Object.prototype.hasOwnProperty.call(attrs, key)) { + hasValue = true; + value = attrs[key]; + break; } - }; - - // 更新缓存 - if (!g.glyphStates) { - g.glyphStates = { [stateName]: glyphAttrs }; - } else if (!g.glyphStates[stateName]) { - g.glyphStates[stateName] = glyphAttrs; } - - return glyphAttrs; - }; - - g.useStates(g.context.states, hasAnimation); + if (!hasValue && defaults && Object.prototype.hasOwnProperty.call(defaults, key)) { + hasValue = true; + value = defaults[key]; + } + const hasOwn = Object.prototype.hasOwnProperty.call(base, key); + if (hasValue) { + if (!hasOwn || (base as any)[key] !== value) { + (patch ??= {})[key] = value; + changed = true; + } + } else if (hasOwn) { + (removedKeys ??= []).push(key); + changed = true; + } + }); + if (changed) { + glyph.commitSubGraphicAttributes(child, patch ?? {}, removedKeys, context); + } } }; protected _createGraphic(attrs: IGlyphGraphicAttribute = {}): IGraphic { const glyph = createGlyph(attrs); - glyph.onBeforeAttributeUpdate = this._onGlyphAttributeUpdate(glyph); - const onAttributeUpdate = glyph.onAttributeUpdate.bind(glyph); - glyph.onAttributeUpdate = (context: any) => { - onAttributeUpdate(context); - if (context?.type === GLYPH_STATE_ATTRIBUTE_UPDATE_TYPE) { - this._syncInheritedStyleAttrs(glyph, glyph.attribute); - } - }; const subMarks = this._subMarks; if (subMarks) { @@ -196,17 +237,12 @@ export abstract class GlyphMark subGraphics.push(subGraphic); subGraphic.name = name; - subGraphic.onBeforeAttributeUpdate = (attrs: any) => { - return attrs; - }; } }); glyph.setSubGraphic(subGraphics); } - (glyph as any).onBeforeAttributeUpdate(attrs); - return glyph; } diff --git a/packages/vchart/src/mark/link-path.ts b/packages/vchart/src/mark/link-path.ts index 3a4eabc791..8e6150c523 100644 --- a/packages/vchart/src/mark/link-path.ts +++ b/packages/vchart/src/mark/link-path.ts @@ -7,6 +7,9 @@ import { GlyphMark, registerGlyphMark } from './glyph'; import type { IGlyph, IPathGraphicAttribute } from '@visactor/vrender-core'; import { createPath, registerPath } from '../vrender-bridge'; import type { Datum } from '../typings/common'; +import { STATE_VALUE_ENUM } from '../compile/mark/interface'; + +const LINK_RUNTIME_STATE_KEYS = { [STATE_VALUE_ENUM.STATE_SANKEY_EMPHASIS]: ['ratio'] }; export const getHorizontalPath = (options: ILinkPathMarkSpec, ratio?: number) => { const curvature = options.curvature ?? 0.5; @@ -156,6 +159,10 @@ export class LinkPathMark extends GlyphMark static readonly type = MarkTypeEnum.linkPath; readonly type = LinkPathMark.type; + protected _getRuntimeStateKeys() { + return LINK_RUNTIME_STATE_KEYS; + } + protected _getDefaultStyle() { const defaultStyle: IMarkStyle = { ...super._getDefaultStyle(), @@ -199,7 +206,7 @@ export class LinkPathMark extends GlyphMark 'align', 'endArrow', 'startArrow', - 'ratio' + 'direction' ]; protected _channelEncoder = { @@ -211,10 +218,7 @@ export class LinkPathMark extends GlyphMark }; protected _positionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { - const newAttrs = { - ...g.attribute, - ...glyphAttrs - }; + const newAttrs = glyphAttrs; const direction = newAttrs.direction ?? this._glyphConfig?.direction; const parsePath = ['vertical', 'TB', 'BT'].includes(direction) ? getVerticalPath : getHorizontalPath; diff --git a/packages/vchart/src/mark/utils/glyph.ts b/packages/vchart/src/mark/utils/glyph.ts index 44267562eb..d0c26e45ed 100644 --- a/packages/vchart/src/mark/utils/glyph.ts +++ b/packages/vchart/src/mark/utils/glyph.ts @@ -1,5 +1,5 @@ import type { IMarkGraphic } from '../interface/common'; -import { addGraphicState, removeGraphicState } from '../../util/graphic-state'; +import { addGraphicState } from '../../util/graphic-state'; export const addRuntimeState = ( g: IMarkGraphic, @@ -14,8 +14,5 @@ export const addRuntimeState = ( g.runtimeStateCache[stateName] = attrs; - if (g.hasState(stateName)) { - removeGraphicState(g, stateName); - } addGraphicState(g, stateName, keepCurrentStates, hasAnimation); }; From 738fec391a1e323780b49b1fe5088f8d0087507a Mon Sep 17 00:00:00 2001 From: xile611 Date: Wed, 16 Sep 2026 11:37:31 +0800 Subject: [PATCH 2/3] fix: upgrade vrender to 1.1.11 --- common/config/rush/pnpm-lock.yaml | 120 ++++++++++++------------- docs/package.json | 6 +- packages/openinula-vchart/package.json | 6 +- packages/react-vchart/package.json | 6 +- packages/vchart-extension/package.json | 10 +-- packages/vchart/package.json | 12 +-- tools/story-player/package.json | 8 +- 7 files changed, 84 insertions(+), 84 deletions(-) diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index bfdb9c2fc4..183f0dbe74 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -37,11 +37,11 @@ importers: specifier: 1.2.4-alpha.5 version: 1.2.4-alpha.5 '@visactor/vrender': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-kits': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vtable': specifier: 1.19.0-alpha.0 version: 1.19.0-alpha.0 @@ -203,11 +203,11 @@ importers: specifier: workspace:2.1.6 version: link:../vchart '@visactor/vrender-core': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-kits': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -294,11 +294,11 @@ importers: specifier: workspace:2.1.6 version: link:../vchart-extension '@visactor/vrender-core': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-kits': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -529,20 +529,20 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-animate': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-components': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-core': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-kits': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vscale': specifier: ~1.0.23 version: 1.0.23 @@ -695,17 +695,17 @@ importers: specifier: ~1.0.23 version: 1.0.23 '@visactor/vrender-animate': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-components': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-core': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-kits': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -1263,14 +1263,14 @@ importers: specifier: workspace:2.1.6 version: link:../../packages/vchart '@visactor/vrender': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-core': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vrender-kits': - specifier: ~1.1.8 - version: 1.1.8 + specifier: 1.1.11 + version: 1.1.11 '@visactor/vutils': specifier: ~1.0.23 version: 1.0.23 @@ -3079,29 +3079,29 @@ packages: '@visactor/vrender-animate@1.0.0-alpha.18': resolution: {integrity: sha512-9kTtvp1ef+1t+AtUiza6A7qBQP7SmvOu3/ILGrqs/HGdZVj1XGjbYvD/X/zwKJ3LEb7gGV5fa8x95e4czTvRSA==} - '@visactor/vrender-animate@1.1.8': - resolution: {integrity: sha512-p6x6rFjInJIIqiPW3bqBIqjU7cIGCYm7asXYTTm6FEauzhSL6TJXxucOKTaEVCTExfHK0xaUwciq5wuaZhiUPA==} + '@visactor/vrender-animate@1.1.11': + resolution: {integrity: sha512-GBES37lUpM6HupDVSIAJz6YVkOaH2hNFRPR43+ZggE97KDeOCqQk+JXcEsrHjX18zFDXLJ2GIjwXvK+En5n8Jw==} '@visactor/vrender-components@1.0.0-alpha.18': resolution: {integrity: sha512-7Euq+ZfswL74n2pgkaqZSsPxoSa5SPIGyXatN1eUrdzM2Z0kX6U0RcJg01fctvRs4op6WhcecRLqGvnHcBeb9Q==} - '@visactor/vrender-components@1.1.8': - resolution: {integrity: sha512-5wF7fIkBQ9ug2LDSWLnrUhzbTVVHsTtuZkXXC528toE1MLHZoVMJIxIdEbN9IdbfImWfPqO+TLRp61rW5oJzVQ==} + '@visactor/vrender-components@1.1.11': + resolution: {integrity: sha512-7qpf59Q8NWYZ3SpSjLyri5S2KmoDXqTCu0X4nGgqtV78aPFgmeHxvsonQu4t8uSF/xK0k3VY6GWtuC++Y9ubLw==} '@visactor/vrender-core@1.0.0-alpha.18': resolution: {integrity: sha512-0ihtNvCyNkOsWPFgRqowHzq0IcQgS2Wl/nPpKbVtxWKveenwlhA+ZKoQvam6VJyBY7jeNe1pROy0mJMDyVAJQw==} - '@visactor/vrender-core@1.1.8': - resolution: {integrity: sha512-DSKEtLniC/7Cd7GxWEJC6o9/6Zf5BisqeecMbK3yqOiM0tdiP7w3OBM8wOsFyuw/Bkfr8sC5bUP0z3wQ1mMHGg==} + '@visactor/vrender-core@1.1.11': + resolution: {integrity: sha512-JmSY4d9PX28cFNBkBY1i/vHhNFpSNmqvHCSI6/QIcfrEsyVyk9HOCke6R6PSagpn5dkSo4ZfnECG6/CwlQUyoQ==} '@visactor/vrender-kits@1.0.0-alpha.18': resolution: {integrity: sha512-Tvolkq+4G8qiPFZo0Aj8M//Yr6jR2h8FNkFEyWM9gbQbEiTkjpmHAJOYnoSsaPtPrcMSlG4EhJSFDk6ymANHVg==} - '@visactor/vrender-kits@1.1.8': - resolution: {integrity: sha512-12IYBY0zpkmGO5H9btRcP0ZamRFQnpmvkZ0gTdU5EI7adMXVDcvLsDMzzRMdbLAqysSRqd1d+UVFY0vSpFQnNA==} + '@visactor/vrender-kits@1.1.11': + resolution: {integrity: sha512-ebDopmBSmz0VXPHiozLDP9WOU10miJ3a0vCQnjeyygD8VglWfA+Yx6Zuwr3osEMX7MRNlCCtcbgAPP1s43JuTA==} - '@visactor/vrender@1.1.8': - resolution: {integrity: sha512-bQS9fSaSry5K7zjKIyP7CGvOga0rXuyJBg3hzkBSMuLwScCafQz8MYJRrG9grKV0cTF12lRY6bq4za4MdXwL3A==} + '@visactor/vrender@1.1.11': + resolution: {integrity: sha512-MBI/XhUuhp1i0zv2dW/DtLpSwKGrdRNPcX+3UMQu5gPKq26zV0DtPfnI2ELwo+8gQtuiZ+qf6pP7I0g/fUGxoQ==} '@visactor/vscale@0.18.18': resolution: {integrity: sha512-iRG4kv+5Fv4KX3AxEfV95XU3I6OmF0QizyAhqHxKa7L1MaT+MRvDDk5zHWf1E8gialLbL2xDe3GnT6g/4u5jhA==} @@ -15066,9 +15066,9 @@ snapshots: '@visactor/vrender-core': 1.0.0-alpha.18 '@visactor/vutils': 1.0.4 - '@visactor/vrender-animate@1.1.8': + '@visactor/vrender-animate@1.1.11': dependencies: - '@visactor/vrender-core': 1.1.8 + '@visactor/vrender-core': 1.1.11 '@visactor/vutils': 1.0.23 '@visactor/vrender-components@1.0.0-alpha.18': @@ -15079,11 +15079,11 @@ snapshots: '@visactor/vscale': 1.0.4 '@visactor/vutils': 1.0.4 - '@visactor/vrender-components@1.1.8': + '@visactor/vrender-components@1.1.11': dependencies: - '@visactor/vrender-animate': 1.1.8 - '@visactor/vrender-core': 1.1.8 - '@visactor/vrender-kits': 1.1.8 + '@visactor/vrender-animate': 1.1.11 + '@visactor/vrender-core': 1.1.11 + '@visactor/vrender-kits': 1.1.11 '@visactor/vscale': 1.0.23 '@visactor/vutils': 1.0.23 @@ -15092,7 +15092,7 @@ snapshots: '@visactor/vutils': 1.0.4 color-convert: 2.0.1 - '@visactor/vrender-core@1.1.8': + '@visactor/vrender-core@1.1.11': dependencies: '@visactor/vutils': 1.0.23 color-convert: 2.0.1 @@ -15106,21 +15106,21 @@ snapshots: lottie-web: 5.13.0 roughjs: 4.5.2 - '@visactor/vrender-kits@1.1.8': + '@visactor/vrender-kits@1.1.11': dependencies: '@resvg/resvg-js': 2.4.1 - '@visactor/vrender-core': 1.1.8 + '@visactor/vrender-core': 1.1.11 '@visactor/vutils': 1.0.23 gifuct-js: 2.1.2 lottie-web: 5.13.0 roughjs: 4.6.6 - '@visactor/vrender@1.1.8': + '@visactor/vrender@1.1.11': dependencies: - '@visactor/vrender-animate': 1.1.8 - '@visactor/vrender-components': 1.1.8 - '@visactor/vrender-core': 1.1.8 - '@visactor/vrender-kits': 1.1.8 + '@visactor/vrender-animate': 1.1.11 + '@visactor/vrender-components': 1.1.11 + '@visactor/vrender-core': 1.1.11 + '@visactor/vrender-kits': 1.1.11 '@visactor/vscale@0.18.18': dependencies: diff --git a/docs/package.json b/docs/package.json index 22be1ae9de..f69c3bc1cb 100644 --- a/docs/package.json +++ b/docs/package.json @@ -19,8 +19,8 @@ "@visactor/vchart-theme": "~1.6.6", "@visactor/vmind": "1.2.4-alpha.5", "@visactor/vutils": "~1.0.23", - "@visactor/vrender": "~1.1.8", - "@visactor/vrender-kits": "~1.1.8", + "@visactor/vrender": "1.1.11", + "@visactor/vrender-kits": "1.1.11", "@visactor/vtable": "1.19.0-alpha.0", "@visactor/vtable-editors": "1.19.0-alpha.0", "@visactor/vtable-gantt": "1.19.0-alpha.0", @@ -58,4 +58,4 @@ "react-device-detect": "^2.2.2", "minimist": "1.2.8" } -} +} \ No newline at end of file diff --git a/packages/openinula-vchart/package.json b/packages/openinula-vchart/package.json index 21cb4d26a1..db692998a3 100644 --- a/packages/openinula-vchart/package.json +++ b/packages/openinula-vchart/package.json @@ -30,8 +30,8 @@ "dependencies": { "@visactor/vchart": "workspace:2.1.6", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "~1.1.8", - "@visactor/vrender-kits": "~1.1.8", + "@visactor/vrender-core": "1.1.11", + "@visactor/vrender-kits": "1.1.11", "react-is": "^18.2.0" }, "devDependencies": { @@ -78,4 +78,4 @@ "access": "public", "registry": "https://registry.npmjs.org/" } -} +} \ No newline at end of file diff --git a/packages/react-vchart/package.json b/packages/react-vchart/package.json index 07b4fc44a2..096008282e 100644 --- a/packages/react-vchart/package.json +++ b/packages/react-vchart/package.json @@ -36,8 +36,8 @@ "@visactor/vchart": "workspace:2.1.6", "@visactor/vchart-extension": "workspace:2.1.6", "@visactor/vutils": "~1.0.23", - "@visactor/vrender-core": "~1.1.8", - "@visactor/vrender-kits": "~1.1.8", + "@visactor/vrender-core": "1.1.11", + "@visactor/vrender-kits": "1.1.11", "react-is": "^18.2.0" }, "devDependencies": { @@ -88,4 +88,4 @@ "access": "public", "registry": "https://registry.npmjs.org/" } -} +} \ No newline at end of file diff --git a/packages/vchart-extension/package.json b/packages/vchart-extension/package.json index e1deac2d5f..e26c6aff54 100644 --- a/packages/vchart-extension/package.json +++ b/packages/vchart-extension/package.json @@ -26,10 +26,10 @@ "start": "ts-node __tests__/runtime/browser/scripts/initVite.ts && vite serve __tests__/runtime/browser" }, "dependencies": { - "@visactor/vrender-core": "~1.1.8", - "@visactor/vrender-kits": "~1.1.8", - "@visactor/vrender-components": "~1.1.8", - "@visactor/vrender-animate": "~1.1.8", + "@visactor/vrender-core": "1.1.11", + "@visactor/vrender-kits": "1.1.11", + "@visactor/vrender-components": "1.1.11", + "@visactor/vrender-animate": "1.1.11", "@visactor/vchart": "workspace:2.1.6", "@visactor/vutils": "~1.0.23", "@visactor/vdataset": "~1.0.23", @@ -71,4 +71,4 @@ "directory": "packages/vchart-extension" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/packages/vchart/package.json b/packages/vchart/package.json index baee74e259..23388fbd51 100644 --- a/packages/vchart/package.json +++ b/packages/vchart/package.json @@ -128,15 +128,15 @@ "@visactor/vdataset": "~1.0.23", "@visactor/vscale": "~1.0.23", "@visactor/vlayouts": "~1.0.23", - "@visactor/vrender": "~1.1.8", - "@visactor/vrender-core": "~1.1.8", - "@visactor/vrender-kits": "~1.1.8", - "@visactor/vrender-components": "~1.1.8", - "@visactor/vrender-animate": "~1.1.8", + "@visactor/vrender": "1.1.11", + "@visactor/vrender-core": "1.1.11", + "@visactor/vrender-kits": "1.1.11", + "@visactor/vrender-components": "1.1.11", + "@visactor/vrender-animate": "1.1.11", "@visactor/vutils-extension": "workspace:2.1.6" }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" } -} +} \ No newline at end of file diff --git a/tools/story-player/package.json b/tools/story-player/package.json index ece3a04f03..f84d74c0cd 100644 --- a/tools/story-player/package.json +++ b/tools/story-player/package.json @@ -56,10 +56,10 @@ "vite": "3.2.6" }, "dependencies": { - "@visactor/vrender-core": "~1.1.8", - "@visactor/vrender-kits": "~1.1.8", + "@visactor/vrender-core": "1.1.11", + "@visactor/vrender-kits": "1.1.11", "@visactor/vchart": "workspace:2.1.6", - "@visactor/vrender": "~1.1.8", + "@visactor/vrender": "1.1.11", "@visactor/vutils": "~1.0.23" } -} +} \ No newline at end of file From 01ac887508f96815edfb3dd7a9b3c59766901c42 Mon Sep 17 00:00:00 2001 From: xile611 Date: Thu, 17 Sep 2026 18:05:13 +0800 Subject: [PATCH 3/3] fix(liquid): preserve explicit fill opacity on glyph waves --- .../fix-sankey-glyph-state-20260915.json | 2 +- ...26-09-17-liquid-fill-opacity-regression.md | 232 ++++++++++++++++++ .../__tests__/unit/mark/glyph-state.test.ts | 28 +++ .../__tests__/unit/series/liquid.test.ts | 65 ++++- packages/vchart/src/mark/liquid.ts | 8 + 5 files changed, 333 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/plans/2026-09-17-liquid-fill-opacity-regression.md diff --git a/common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json b/common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json index 0cb37243a1..21b3296cc2 100644 --- a/common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json +++ b/common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@visactor/vchart", - "comment": "修复 Glyph 状态更新未重新编码子图形导致的桑基图 hover 残留、关联流量高亮比例和取消恢复异常;统一共享运行时状态与子图形属性撤销。需要配套包含 Glyph 派生同步接口的 VRender 版本。", + "comment": "修复 Glyph 状态更新未重新编码子图形导致的桑基图 hover 残留、关联流量高亮比例和取消恢复异常;统一共享运行时状态与子图形属性撤销。需要配套包含 Glyph 派生同步接口的 VRender 版本。同时修复 Liquid 显式 fillOpacity 在初始渲染、配置更新及状态切换中未作用于子波浪的问题,并在退出状态时恢复普通样式或默认透明度。", "type": "patch" } ], diff --git a/docs/superpowers/plans/2026-09-17-liquid-fill-opacity-regression.md b/docs/superpowers/plans/2026-09-17-liquid-fill-opacity-regression.md new file mode 100644 index 0000000000..89d77bf6b4 --- /dev/null +++ b/docs/superpowers/plans/2026-09-17-liquid-fill-opacity-regression.md @@ -0,0 +1,232 @@ +# Liquid 显式透明度回归修复计划与执行记录 + +**Goal:** 修复 PR #4687 导致的 Liquid 显式 `fillOpacity` 失效,保证初始配置、配置更新和状态切换都正确作用于三个子波浪。 + +**Architecture:** 在 `LiquidMark` 中声明 `fillOpacity` 通道编码,复用 `GlyphMark` 的输入变化检测、派生属性提交和默认属性恢复。用户显式值覆盖三个波浪的默认透明度;有效属性变为 `undefined` 时,由现有通道撤销流程恢复各自默认值。 + +**Tech Stack:** TypeScript、VChart、VRender 1.1.11、Jest / ts-jest / jest-electron。 + +## Global Constraints + +- 基线:`codex/fix-sankey-glyph-state`,提交 `738fec391a1e323780b49b1fe5088f8d0087507a`。 +- 运行时代码范围为 `packages/vchart/src/mark/liquid.ts`,复用现有 `_channelEncoder` 标准路径。 +- 保持当前 VRender 1.1.11 依赖;不新增包或调整锁文件。 +- 显式 `fillOpacity: v` 的结果是 `[v, v, v]`,保持原有覆盖语义;不改为与默认透明度相乘。 +- 未配置时默认值仍为 `[1, 0.66, 0.33]`;退出状态时恢复当前普通样式,有普通样式则优先恢复该值。 +- 透明度变化不重算波浪点数组,不把 `fillOpacity` 加入 `_positionChannels`。 +- 不恢复 Glyph 全量样式复制,不改变 Ripple、BoxPlot、Sankey 或公共状态接口。 +- 使用中文变更说明;修复源码、测试、变更说明与本执行记录一并提交。 + +--- + +## 依据与方案选择 + +[Review 原文](https://github.com/VisActor/VChart/pull/4687#discussion_r4035236608)。已在上述基线和正式 VRender 1.1.11 下复现:初始 `fillOpacity: 0`、`updateSpecSync` 更新为 0、hover 配置为 0 时,父 Glyph 值为 0,三个子波浪仍为 `[1, 0.66, 0.33]`。现有 Glyph 的 11 项测试通过,但 Liquid 用例只检查颜色变化和默认透明度。 + +| 方案 | 判断 | +| --- | --- | +| Liquid 声明 `fillOpacity` 通道编码 | 采用。直接表达 Liquid 的属性覆盖规则,复用已有缓存和恢复流程。 | +| 恢复 Glyph 对所有子图形的样式复制 | 不采用。扩大热路径成本和影响范围,还可能覆盖其他 Glyph 的派生样式。 | +| 将 `fillOpacity` 加入 Liquid 几何编码 | 不采用。透明度变化会无谓重建三组波浪点。 | + +现有 `GlyphMark._encodeGlyph` 已在通道输入变为 `undefined` 时撤销该通道输出,并回退到 `_subMarks.defaultAttributes`;本修复不另设状态缓存或恢复钩子。 + +## Task 1:恢复 Liquid 透明度契约并完成定向验证 + +**Files:** + +- Modify: `packages/vchart/src/mark/liquid.ts`:声明透明度通道编码。 +- Test: `packages/vchart/__tests__/unit/mark/glyph-state.test.ts`:透明度变化、状态恢复和几何复用。 +- Test: `packages/vchart/__tests__/unit/series/liquid.test.ts`:真实图表的初始 spec、更新 spec 和配置状态。 +- Modify: `common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json`:补充 Liquid 修复说明。 + +**Interfaces:** + +- Consumes: `GlyphMark._channelEncoder: Record Record>`,以及既有 `createGraphic(mark, attrs, datum?)` 测试辅助函数。 +- Produces: `LiquidMark._channelEncoder.fillOpacity(value: number)` 返回三个子波浪的透明度属性;不新增公共 API。 + +- [x] **Step 1:添加图元级回归,锁定状态恢复与几何成本。** + +在 `glyph-state.test.ts` 现有 Liquid 测试后追加以下用例,复用已有注册和 `createGraphic`;保留原有测试。 + +```ts +test.each([undefined, 0.4])('liquid restores normal fillOpacity %s without rebuilding points', normalOpacity => { + const mark: any = new LiquidMark('liquid', markContext); + const attrs: any = { y: 20, height: 100, wave: 0, fill: 'red' }; + if (normalOpacity !== undefined) { + attrs.fillOpacity = normalOpacity; + } + const g = createGraphic(mark, attrs); + const values = () => g.getSubGraphic().map((child: any) => child.attribute.fillOpacity); + const normal = normalOpacity === undefined ? [1, 0.66, 0.33] : [normalOpacity, normalOpacity, normalOpacity]; + const points = g.getSubGraphic().map((child: any) => child.attribute.points); + const encode = jest.spyOn(mark, '_positionEncoder'); + + expect(values()).toEqual(normal); + g.states = { hover: { fillOpacity: 0 } }; + g.setStates(['hover'], false); + expect(values()).toEqual([0, 0, 0]); + g.clearStates(false); + expect(values()).toEqual(normal); + + g.setAttribute('fillOpacity', 0.6); + expect(values()).toEqual([0.6, 0.6, 0.6]); + expect(encode).not.toHaveBeenCalled(); + g.getSubGraphic().forEach((child: any, i: number) => { + expect(child.attribute.points).toBe(points[i]); + }); + g.release(); +}); +``` + +- [x] **Step 2:添加真实图表回归,覆盖 review 的三个入口。** + +在 `liquid.test.ts` 中,将现有 `LiquidSeries` 导入改为 `import VChart, { LiquidSeries, registerLiquidChart } from '../../../src';`,另加 `import { createDiv, removeDom } from '../../util/dom';`。保留现有几何用例,追加: + +```ts +describe('Liquid explicit fillOpacity', () => { + let chart: VChart; + let dom: HTMLElement; + const spec = (liquid: any): any => ({ + type: 'liquid', + width: 200, + height: 200, + data: [{ id: 'data', values: [{ value: 0.5 }] }], + valueField: 'value', + animation: false, + liquid + }); + const render = (liquid: any) => { + chart = new VChart(spec(liquid), { dom, animation: false }); + chart.renderSync(); + }; + const glyph = (): any => chart.getChart().getAllSeries()[0].getMarkInName('liquid')!.getGraphics()[0]; + const values = () => glyph().getSubGraphic().map((child: any) => child.attribute.fillOpacity); + + beforeAll(() => registerLiquidChart()); + beforeEach(() => { + dom = createDiv(); + }); + afterEach(() => { + chart?.release(); + removeDom(dom); + }); + + test.each([0, 0.4, 1])('initial fillOpacity %s overrides all waves', value => { + render({ style: { fillOpacity: value } }); + expect(glyph().attribute.fillOpacity).toBe(value); + expect(values()).toEqual([value, value, value]); + }); + + test('updateSpecSync changes the opacity of all waves', () => { + render({}); + expect(values()).toEqual([1, 0.66, 0.33]); + for (const value of [0, 0.4, 1]) { + chart.updateSpecSync(spec({ style: { fillOpacity: value } })); + expect(glyph().attribute.fillOpacity).toBe(value); + expect(values()).toEqual([value, value, value]); + } + }); + + test.each([undefined, 0.4])('configured hover restores normal opacity %s', normalOpacity => { + render({ + ...(normalOpacity === undefined ? {} : { style: { fillOpacity: normalOpacity } }), + state: { hover: { fillOpacity: 0 } } + }); + const normal = normalOpacity === undefined ? [1, 0.66, 0.33] : [normalOpacity, normalOpacity, normalOpacity]; + expect(values()).toEqual(normal); + glyph().setStates(['hover'], false); + expect(glyph().attribute.fillOpacity).toBe(0); + expect(values()).toEqual([0, 0, 0]); + glyph().clearStates(false); + expect(values()).toEqual(normal); + }); +}); +``` + +必须显式调用 `registerLiquidChart()`;当前入口没有默认注册 Liquid,否则失败原因会变成 `init chart fail`,无法验证透明度问题。状态用例通过真实 spec 建立共享状态定义,再驱动 Glyph 状态入口;本次不重复测试鼠标拾取系统。 + +- [x] **Step 3:在未修复源码上运行新增测试,确认失败原因。** + +以下命令均在 `packages/vchart` 执行: + +```sh +./node_modules/.bin/jest --runInBand --runTestsByPath __tests__/unit/mark/glyph-state.test.ts __tests__/unit/series/liquid.test.ts +``` + +预期新增用例在三个子波浪的透明度断言失败,收到 `[1, 0.66, 0.33]`;不是初始化、导入或类型错误。记录失败后再修改运行时代码。 + +- [x] **Step 4:在 Liquid 中声明最小通道编码。** + +在 `liquid.ts` 的 `_positionChannels` 前加入: + +```ts +protected _channelEncoder = { + fillOpacity: (fillOpacity: number) => ({ + wave0: { fillOpacity }, + wave1: { fillOpacity }, + wave2: { fillOpacity } + }) +}; +``` + +`0` 是有效输入,不使用 truthy 判断;`undefined` 的撤销和默认值恢复由现有 Glyph 流程处理。编码函数只产生透明度属性,不读取 datum、不生成 points、不直接调用子图形 `setAttributes`。 + +- [x] **Step 5:完成相关回归和静态检查。** + +在 `packages/vchart` 执行: + +```sh +./node_modules/.bin/jest --runInBand --runTestsByPath __tests__/unit/mark/glyph-state.test.ts __tests__/unit/series/liquid.test.ts __tests__/unit/series/sankey-emphasis-state.test.ts +./node_modules/.bin/tsc --noEmit --project tsconfig.json +./node_modules/.bin/eslint src/mark/liquid.ts __tests__/unit/mark/glyph-state.test.ts __tests__/unit/series/liquid.test.ts --quiet +./node_modules/.bin/prettier --check src/mark/liquid.ts __tests__/unit/mark/glyph-state.test.ts __tests__/unit/series/liquid.test.ts +``` + +预期全部通过。Glyph 套件同时覆盖 Ripple、BoxPlot、Sankey link、图元复用和渐进创建;Sankey 系列套件保护本 PR 原始修复。纯透明度路径的几何编码调用次数和 points 引用断言作为本次性能验收,不另建大规模基准。 + +- [x] **Step 6:补充变更说明,检查差异并形成单个修复提交。** + +在现有 changes 文件的 `comment` 末尾追加以下中文句子,保留 `patch` 类型和其他原始内容: + +```text +同时修复 Liquid 显式 fillOpacity 在初始渲染、配置更新及状态切换中未作用于子波浪的问题,并在退出状态时恢复普通样式或默认透明度。 +``` + +在仓库根目录执行: + +```sh +git diff --check +git diff --stat +git diff -- packages/vchart/src/mark/liquid.ts packages/vchart/__tests__/unit/mark/glyph-state.test.ts packages/vchart/__tests__/unit/series/liquid.test.ts common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json +git add packages/vchart/src/mark/liquid.ts packages/vchart/__tests__/unit/mark/glyph-state.test.ts packages/vchart/__tests__/unit/series/liquid.test.ts common/changes/@visactor/vchart/fix-sankey-glyph-state-20260915.json +git add docs/superpowers/plans/2026-09-17-liquid-fill-opacity-regression.md +git commit -m "fix(liquid): preserve explicit fill opacity on glyph waves" +``` + +交付时列出实际测试结果及提交号;push、回复 review 和 resolve review 不包含在本次规划范围内。 + +## 完成标准 + +- 显式 0、0.4、1 在初始配置和配置更新中分别得到三个相同的子波浪透明度。 +- hover 的透明度覆盖生效,退出后恢复普通样式;未配置普通样式时恢复三个默认值。 +- 透明度更新和状态切换不触发位置编码,保留已有 points 引用。 +- 相关 Glyph、Liquid、Sankey 测试和定向静态检查通过。 +- 运行时代码仅修改 Liquid 通道定义,变更记录准确描述修复行为。 + +## 计划自查 + +- 已将 review 的三个失败入口映射到具体测试。 +- 已明确显式覆盖、未配置默认值、状态恢复和性能约束。 +- 已复用现有编码和恢复接口;无新增公共接口、依赖或通用兜底。 +- 已按单个修复单元执行,未调整依赖或扩大运行时代码范围。 + +## 执行结果(2026-09-17) + +- 修改运行时代码前,新增的 2 项图元用例和 6 项真实图表用例均在子波浪透明度断言处失败,收到默认值 `[1, 0.66, 0.33]`;无初始化或导入错误。 +- 修复后 3 个测试套件、25 项测试全部通过:Glyph 13 项、Liquid 7 项、Sankey 5 项。 +- 已验证显式 0、0.4、1 的初始渲染和配置更新,以及 hover 退出后恢复普通样式或默认值。 +- 图元级用例验证了透明度更新和状态切换期间 `_positionEncoder` 调用为 0,子波浪 points 引用保持不变。 +- `tsc --noEmit`、定向 ESLint、Prettier 和 `git diff --check` 均通过;Prettier 仅将测试中的链式 getter 换行。 +- 运行时代码仅在 `LiquidMark` 中增加 8 行通道编码,复用既有撤销与恢复逻辑。 +- 本轮未 push,未回复或 resolve 远端 review。 diff --git a/packages/vchart/__tests__/unit/mark/glyph-state.test.ts b/packages/vchart/__tests__/unit/mark/glyph-state.test.ts index 0dbc92feeb..c3d5f38c73 100644 --- a/packages/vchart/__tests__/unit/mark/glyph-state.test.ts +++ b/packages/vchart/__tests__/unit/mark/glyph-state.test.ts @@ -153,6 +153,34 @@ test('liquid retains child opacity and does not rebuild points for a paint-only expect(g.getSubGraphic()[0].attribute.points[0].x).toBe(-450); }); +test.each([undefined, 0.4])('liquid restores normal fillOpacity %s without rebuilding points', normalOpacity => { + const mark: any = new LiquidMark('liquid', markContext); + const attrs: any = { y: 20, height: 100, wave: 0, fill: 'red' }; + if (normalOpacity !== undefined) { + attrs.fillOpacity = normalOpacity; + } + const g = createGraphic(mark, attrs); + const values = () => g.getSubGraphic().map((child: any) => child.attribute.fillOpacity); + const normal = normalOpacity === undefined ? [1, 0.66, 0.33] : [normalOpacity, normalOpacity, normalOpacity]; + const points = g.getSubGraphic().map((child: any) => child.attribute.points); + const encode = jest.spyOn(mark, '_positionEncoder'); + + expect(values()).toEqual(normal); + g.states = { hover: { fillOpacity: 0 } }; + g.setStates(['hover'], false); + expect(values()).toEqual([0, 0, 0]); + g.clearStates(false); + expect(values()).toEqual(normal); + + g.setAttribute('fillOpacity', 0.6); + expect(values()).toEqual([0.6, 0.6, 0.6]); + expect(encode).not.toHaveBeenCalled(); + g.getSubGraphic().forEach((child: any, i: number) => { + expect(child.attribute.points).toBe(points[i]); + }); + g.release(); +}); + test.each([0, 1])('ratio endpoint %s has the correct foreground and full background thickness', ratio => { const mark: any = new LinkPathMark('link', markContext); const g = createGraphic(mark, linkAttrs); diff --git a/packages/vchart/__tests__/unit/series/liquid.test.ts b/packages/vchart/__tests__/unit/series/liquid.test.ts index 7717d0c994..4576f941d5 100644 --- a/packages/vchart/__tests__/unit/series/liquid.test.ts +++ b/packages/vchart/__tests__/unit/series/liquid.test.ts @@ -1,8 +1,9 @@ import { DataSet } from '@visactor/vdataset'; import type { ISeriesOption } from '../../../src/series/interface'; -import { LiquidSeries } from '../../../src'; +import VChart, { LiquidSeries, registerLiquidChart } from '../../../src'; import { initChartDataSet, seriesOption } from '../../util/context'; import { TestRegion } from '../../util/factory/region'; +import { createDiv, removeDom } from '../../util/dom'; const dataSet = new DataSet(); initChartDataSet(dataSet); @@ -52,3 +53,65 @@ describe('[Domain-Series-Liquid] Liquid Series', () => { expect((liquid as any)._getLiquidPosY()).toBe(0); }); }); + +describe('Liquid explicit fillOpacity', () => { + let chart: VChart; + let dom: HTMLElement; + const spec = (liquid: any): any => ({ + type: 'liquid', + width: 200, + height: 200, + data: [{ id: 'data', values: [{ value: 0.5 }] }], + valueField: 'value', + animation: false, + liquid + }); + const render = (liquid: any) => { + chart = new VChart(spec(liquid), { dom, animation: false }); + chart.renderSync(); + }; + const glyph = (): any => chart.getChart().getAllSeries()[0].getMarkInName('liquid')!.getGraphics()[0]; + const values = () => + glyph() + .getSubGraphic() + .map((child: any) => child.attribute.fillOpacity); + + beforeAll(() => registerLiquidChart()); + beforeEach(() => { + dom = createDiv(); + }); + afterEach(() => { + chart?.release(); + removeDom(dom); + }); + + test.each([0, 0.4, 1])('initial fillOpacity %s overrides all waves', value => { + render({ style: { fillOpacity: value } }); + expect(glyph().attribute.fillOpacity).toBe(value); + expect(values()).toEqual([value, value, value]); + }); + + test('updateSpecSync changes the opacity of all waves', () => { + render({}); + expect(values()).toEqual([1, 0.66, 0.33]); + for (const value of [0, 0.4, 1]) { + chart.updateSpecSync(spec({ style: { fillOpacity: value } })); + expect(glyph().attribute.fillOpacity).toBe(value); + expect(values()).toEqual([value, value, value]); + } + }); + + test.each([undefined, 0.4])('configured hover restores normal opacity %s', normalOpacity => { + render({ + ...(normalOpacity === undefined ? {} : { style: { fillOpacity: normalOpacity } }), + state: { hover: { fillOpacity: 0 } } + }); + const normal = normalOpacity === undefined ? [1, 0.66, 0.33] : [normalOpacity, normalOpacity, normalOpacity]; + expect(values()).toEqual(normal); + glyph().setStates(['hover'], false); + expect(glyph().attribute.fillOpacity).toBe(0); + expect(values()).toEqual([0, 0, 0]); + glyph().clearStates(false); + expect(values()).toEqual(normal); + }); +}); diff --git a/packages/vchart/src/mark/liquid.ts b/packages/vchart/src/mark/liquid.ts index 6813856a62..5f35a29eeb 100644 --- a/packages/vchart/src/mark/liquid.ts +++ b/packages/vchart/src/mark/liquid.ts @@ -46,6 +46,14 @@ export class LiquidMark extends GlyphMark implements ILiquidMar } }; + protected _channelEncoder = { + fillOpacity: (fillOpacity: number) => ({ + wave0: { fillOpacity }, + wave1: { fillOpacity }, + wave2: { fillOpacity } + }) + }; + protected _positionChannels: string[] = ['wave', 'y', 'height']; protected _positionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => {