Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
M
model_analysis
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
model_analysis
Commits
87f2ef5a
Commit
87f2ef5a
authored
Sep 08, 2026
by
Andy-bubu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add Xinyuan pie chart palette foundation
parent
f8264ec9
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
497 additions
and
9 deletions
+497
-9
ui-architecture.md
docs/ui-architecture.md
+7
-3
ui-migration-checklist.md
docs/ui-migration-checklist.md
+5
-1
xinyuan-design-system.md
docs/xinyuan-design-system.md
+11
-2
xinyuan-ui-audit.md
docs/xinyuan-ui-audit.md
+19
-3
XinyuanEChart.jsx
src/components/XinyuanEChart.jsx
+209
-0
chartPalette.js
src/design-system/chartPalette.js
+212
-0
theme.css
src/design-system/theme.css
+17
-0
tokens.css
src/design-system/tokens.css
+17
-0
No files found.
docs/ui-architecture.md
View file @
87f2ef5a
...
@@ -10,8 +10,9 @@
...
@@ -10,8 +10,9 @@
2.
`src/tailwind.css`
:Tailwind、HeroUI、HeroUI Pro CSS 入口;使用
`source(none)`
和显式源码目录,排除 Umi 生成目录。
2.
`src/tailwind.css`
:Tailwind、HeroUI、HeroUI Pro CSS 入口;使用
`source(none)`
和显式源码目录,排除 Umi 生成目录。
3.
`src/design-system/theme.css`
:不可修改的权威 HeroUI 主题。
3.
`src/design-system/theme.css`
:不可修改的权威 HeroUI 主题。
4.
`src/design-system/tokens.css`
:Skill 批准的鑫元扩展 Token。
4.
`src/design-system/tokens.css`
:Skill 批准的鑫元扩展 Token。
5.
`src/design-system/shell.css`
:基准 Shell、列表、分页、KPI 和响应式几何。
5.
`src/design-system/chartPalette.js`
:普通图表与 Pie/Donut 独立 Token 的解析、稳定 key 哈希和最多 16 色冲突消解。
6.
页面 JSX:仅业务组合和布局,不包含私有组件外观。
6.
`src/design-system/shell.css`
:基准 Shell、列表、分页、KPI 和响应式几何。
7.
页面 JSX:仅业务组合和布局,不包含私有组件外观。
## 字体稳定性
## 字体稳定性
...
@@ -28,6 +29,7 @@
...
@@ -28,6 +29,7 @@
-
`XinyuanModal.jsx`
:HeroUI
`Modal.Root > Backdrop > Container > Dialog`
的业务无关组合,集中实现 320–1216px 设计系统宽度梯度和 28px 标准内容 inset;扩展尺寸由官方
`lg`
Container 加共享最大宽度实现,不向 HeroUI v3 传递无效的
`xl/4xl`
size。圆角、背景、阴影和交互状态仍由 HeroUI 官方 Modal 样式负责。
-
`XinyuanModal.jsx`
:HeroUI
`Modal.Root > Backdrop > Container > Dialog`
的业务无关组合,集中实现 320–1216px 设计系统宽度梯度和 28px 标准内容 inset;扩展尺寸由官方
`lg`
Container 加共享最大宽度实现,不向 HeroUI v3 传递无效的
`xl/4xl`
size。圆角、背景、阴影和交互状态仍由 HeroUI 官方 Modal 样式负责。
-
`ModelMonitorShared.jsx`
:业务中性的 SearchField、DataGrid、Pagination、ActionBar、状态 Chip、动态字段组合。
-
`ModelMonitorShared.jsx`
:业务中性的 SearchField、DataGrid、Pagination、ActionBar、状态 Chip、动态字段组合。
-
`XinyuanAutocomplete.jsx`
:HeroUI Autocomplete/SearchField/ListBox 组合,暴露当前 HeroUI 值契约,不兼容旧 Select props。
-
`XinyuanAutocomplete.jsx`
:HeroUI Autocomplete/SearchField/ListBox 组合,暴露当前 HeroUI 值契约,不兼容旧 Select props。
-
`XinyuanEChart.jsx`
:业务中立 ECharts 适配器;保留 ARIA、键盘焦点、Tooltip、resize/globalout 和页面事件。只有
`type: 'pie'`
的 series 会被强制映射到
`--rc-pie-chart-*`
,Bar/Line/Ranking 仍走普通
`--rc-chart-*`
。
-
页面:直接组合 HeroUI/HeroUI Pro,不通过 Catalyst、Headless UI 或旧 primitive wrapper。
-
页面:直接组合 HeroUI/HeroUI Pro,不通过 Catalyst、Headless UI 或旧 primitive wrapper。
## 数据流
## 数据流
...
@@ -36,6 +38,8 @@
...
@@ -36,6 +38,8 @@
通知是本项目唯一获准新增的跨应用能力。
`/notifications`
复用既有
`/notice/customer/*`
API;模型分析不引入
`subjectView`
、
`shortTitle`
、View 授权或材料中心附件适配代码。
通知是本项目唯一获准新增的跨应用能力。
`/notifications`
复用既有
`/notice/customer/*`
API;模型分析不引入
`subjectView`
、
`shortTitle`
、View 授权或材料中心附件适配代码。
未来 Pie/Donut 页面必须把完整稳定分类 key 集合传给
`XinyuanEChart.pieCategoryKeys`
。适配器先建立确定性颜色映射,再对当前可见数据着色,使排序、筛选、Tab 和主题变化不改变分类身份色;页面不得提供
`itemStyle.color`
私有覆盖。Pie 标签、引导线、图例、Tooltip、圆环半径、布局和点击回调仍由页面业务 option 决定,适配器不改写这些协议。
## 删除边界
## 删除边界
`src/catalyst-ui-kit`
、不可达资源预约模板及
ECharts 统计模板已从仓库移除。它们不在
`.umirc.ts`
的活动路由中,备份为
`/tmp/model-analysis-legacy-ui.iaSi6Y`
。
`src/catalyst-ui-kit`
、不可达资源预约模板及
旧 ECharts 统计页面已从仓库移除。它们不在
`.umirc.ts`
的活动路由中,备份为
`/tmp/model-analysis-legacy-ui.iaSi6Y`
。保留的
`XinyuanEChart`
是未来图表共用的业务中立基础设施,不包含路由、统计口径或测试数据
。
docs/ui-migration-checklist.md
View file @
87f2ef5a
...
@@ -20,9 +20,12 @@
...
@@ -20,9 +20,12 @@
-
[
x
]
弹窗使用 HeroUI Modal/AlertDialog;全部业务 Modal 接入共享 320–1216px 尺寸梯度,审批详情使用单一 secondary Surface
-
[
x
]
弹窗使用 HeroUI Modal/AlertDialog;全部业务 Modal 接入共享 320–1216px 尺寸梯度,审批详情使用单一 secondary Surface
-
[
x
]
批量同步弹窗使用 HeroUI Pro Stepper/ListView、HeroUI ScrollShadow 和正确 Checkbox anatomy;移除手写灰色模型/批次分组及重复 Surface
-
[
x
]
批量同步弹窗使用 HeroUI Pro Stepper/ListView、HeroUI ScrollShadow 和正确 Checkbox anatomy;移除手写灰色模型/批次分组及重复 Surface
-
[
x
]
KPI 使用 HeroUI Pro compound API 与 16px grid gap
-
[
x
]
KPI 使用 HeroUI Pro compound API 与 16px grid gap
-
[
x
]
Catalyst、Headless UI、
ECharts
、原生可见 select/date/file 和旧兼容包装仓库扫描为零
-
[
x
]
Catalyst、Headless UI、
旧 ECharts 业务页面
、原生可见 select/date/file 和旧兼容包装仓库扫描为零
-
[
x
]
活动页面私有颜色/背景/描边/圆角/阴影类扫描为零
-
[
x
]
活动页面私有颜色/背景/描边/圆角/阴影类扫描为零
-
[
x
]
业务 API、参数、校验、回调、状态流和请求时机保持不变
-
[
x
]
业务 API、参数、校验、回调、状态流和请求时机保持不变
-
[
x
]
Pie/Donut 新增独立
`--xinyuan-pie-chart-*`
/
`--rc-pie-chart-*`
16 色 Token;普通 Bar/Line/Ranking、KPI、品牌色和 HeroUI Primary 保持不变
-
[
x
]
新增
`createResolvedXinyuanPieChartColorMap()`
稳定 key 映射和最多 16 色冲突消解,禁用绿色、青绿色、荧光色和彩虹色
-
[
x
]
新增共享
`XinyuanEChart`
入口,仅对
`series.type === 'pie'`
应用专用色板并保留布局、标签、图例、Tooltip、ARIA 和事件协议
## 最终命令
## 最终命令
...
@@ -42,6 +45,7 @@
...
@@ -42,6 +45,7 @@
-
[
x
]
未读 0/1/99/100、消息/待办、全部/未读、搜索、单条/全部已读、动态 action、站内外跳转、HTML 纯文本、Pagination、EmptyState、Toast
-
[
x
]
未读 0/1/99/100、消息/待办、全部/未读、搜索、单条/全部已读、动态 action、站内外跳转、HTML 纯文本、Pagination、EmptyState、Toast
-
[
x
]
Navbar 36px Bell/主题按钮、8px 间距、48/32/16px gutter;移动 offcanvas、Escape、无页面横向溢出
-
[
x
]
Navbar 36px Bell/主题按钮、8px 间距、48/32/16px gutter;移动 offcanvas、Escape、无页面横向溢出
-
[
x
]
最终通知验收控制台 0 error / 0 warning
-
[
x
]
最终通知验收控制台 0 error / 0 warning
-
[
]
Pie/Donut 真实数据浏览器验收:当前应用没有活动饼图/环形图或真实图表路由,未制造测试数据;首个真实调用接入时验证 2/3/4/6/6+ 分类、浅色/深色、筛选/排序稳定性及 Tooltip/Legend 一致性
-
[
]
真实通知数据端到端验收:Andy 会话调用
`/notice/customer/count`
和
`/notice/customer/query`
返回 401
-
[
]
真实通知数据端到端验收:Andy 会话调用
`/notice/customer/count`
和
`/notice/customer/query`
返回 401
浏览器结构和交互验收所用通知响应仅存在于 Playwright route 内存,不写入仓库、不作为业务数据。真实接口 401 继续交由平台权限/环境修复,前端未伪造成功状态或绕过既有请求拦截器。
浏览器结构和交互验收所用通知响应仅存在于 Playwright route 内存,不写入仓库、不作为业务数据。真实接口 401 继续交由平台权限/环境修复,前端未伪造成功状态或绕过既有请求拦截器。
docs/xinyuan-design-system.md
View file @
87f2ef5a
...
@@ -7,7 +7,7 @@
...
@@ -7,7 +7,7 @@
## 基础约束
## 基础约束
-
页面和共享组件直接使用
`@heroui/react`
、
`@heroui-pro/react`
及官方子路径。
-
页面和共享组件直接使用
`@heroui/react`
、
`@heroui-pro/react`
及官方子路径。
-
权威
`theme.css`
原样加载,SHA-256 为
`a0bf6f243aaf2c94cc8b577145d33d765f8ecab863b3a3b4a358bad4d1a05901`
;
`tokens.css`
仅提供 Skill 批准的
扩展 Token。
-
权威
`theme.css`
保持 HeroUI 核心主题不变;仅追加 Skill 批准的 Pie/Donut 专用图表 Token。
`tokens.css`
只提供对应项目别名和其他已批准
扩展 Token。
-
Inter 400/500/600 拉丁子集由
`.umirc.ts`
生成的常驻
`public/fonts/inter.css`
link 加载;不得在
`tailwind.css`
中导入字体 CSS,避免 Umi HMR 替换
`umi.css`
时造成英文、数字和符号字体回退抖动。
-
Inter 400/500/600 拉丁子集由
`.umirc.ts`
生成的常驻
`public/fonts/inter.css`
link 加载;不得在
`tailwind.css`
中导入字体 CSS,避免 Umi HMR 替换
`umi.css`
时造成英文、数字和符号字体回退抖动。
-
统一文字层级:正文 14px/400,次要说明 13px/400,模块标题 16px/600,页面分区标题
-
统一文字层级:正文 14px/400,次要说明 13px/400,模块标题 16px/600,页面分区标题
18px/700,Navbar 标题 20px/600,表头和状态 Chip 12px/500;对应 Token 为
18px/700,Navbar 标题 20px/600,表头和状态 Chip 12px/500;对应 Token 为
...
@@ -36,9 +36,18 @@
...
@@ -36,9 +36,18 @@
-
弹窗:HeroUI Modal/AlertDialog;统一通过 HeroUI 原生
`XinyuanModal`
compound 组合解析尺寸梯度:
`xs`
320px、
`sm`
384px、
`md`
480px、
`lg`
640px、
`xl`
736px、
`2xl`
840px、
`3xl`
960px、
`4xl`
1088px、
`5xl`
1216px。确认弹窗使用
`md`
,短单列表单使用
`lg`
,模型归档双列表单使用
`2xl`
,内容型详情/转发/批量同步使用
`4xl`
;查看态仅一个 secondary Surface。禁止将 HeroUI v3 不支持的
`xl`
至
`5xl`
直接传给
`Modal.Container`
。
-
弹窗:HeroUI Modal/AlertDialog;统一通过 HeroUI 原生
`XinyuanModal`
compound 组合解析尺寸梯度:
`xs`
320px、
`sm`
384px、
`md`
480px、
`lg`
640px、
`xl`
736px、
`2xl`
840px、
`3xl`
960px、
`4xl`
1088px、
`5xl`
1216px。确认弹窗使用
`md`
,短单列表单使用
`lg`
,模型归档双列表单使用
`2xl`
,内容型详情/转发/批量同步使用
`4xl`
;查看态仅一个 secondary Surface。禁止将 HeroUI v3 不支持的
`xl`
至
`5xl`
直接传给
`Modal.Container`
。
-
普通 Modal 保持 HeroUI 官方 24px 圆角和 Overlay shadow,统一使用 28px panel inset;Modal 内承担真实 L2 边界的
`Surface secondary`
使用基准项目一致的
`rounded-2xl`
(16px),不得保留大面积直角灰色块。关闭按钮直接使用无子节点的
`Modal.CloseTrigger`
,由 HeroUI 渲染原生 16px CloseIcon,禁止替换为受字体基线影响的第三方图标;关闭按钮、标题、正文和 Footer 对齐同一内容边界,标题到正文 12px、正文到 Footer 24px。批量同步使用紧凑 HeroUI Pro Stepper,年份工具区使用正确 Checkbox compound anatomy,批次选择使用单一 HeroUI Pro ListView
`primary`
原生多选和 HeroUI ScrollShadow,不使用页面自制灰色分组块或重复 Surface。
-
普通 Modal 保持 HeroUI 官方 24px 圆角和 Overlay shadow,统一使用 28px panel inset;Modal 内承担真实 L2 边界的
`Surface secondary`
使用基准项目一致的
`rounded-2xl`
(16px),不得保留大面积直角灰色块。关闭按钮直接使用无子节点的
`Modal.CloseTrigger`
,由 HeroUI 渲染原生 16px CloseIcon,禁止替换为受字体基线影响的第三方图标;关闭按钮、标题、正文和 Footer 对齐同一内容边界,标题到正文 12px、正文到 Footer 24px。批量同步使用紧凑 HeroUI Pro Stepper,年份工具区使用正确 Checkbox compound anatomy,批次选择使用单一 HeroUI Pro ListView
`primary`
原生多选和 HeroUI ScrollShadow,不使用页面自制灰色分组块或重复 Surface。
-
KPI:HeroUI Pro KPI compound API;四项使用
`.rc-kpi-grid`
16px gap,标题不附加说明图标。
-
KPI:HeroUI Pro KPI compound API;四项使用
`.rc-kpi-grid`
16px gap,标题不附加说明图标。
-
当前活动业务没有图表;不得为视觉验收制造图表或测试业务数据。未来
增加时只使用 HeroUI Pro chart 与 ChartTooltip
。
-
当前活动业务没有图表;不得为视觉验收制造图表或测试业务数据。未来
图表由 HeroUI/HeroUI Pro Card 承载,使用共享
`XinyuanEChart`
适配器渲染,不在页面创建私有主题,也不新增 Recharts 调用点
。
-
通知中心使用 HeroUI Tabs、SearchField、Card、Avatar、Chip、Pagination、Toast 和 HeroUI Pro EmptyState;一级模块桌面间距 32px、移动端 24px,工具栏间距 16px;页面不设置组件背景、颜色、描边、圆角、阴影或状态皮肤。
-
通知中心使用 HeroUI Tabs、SearchField、Card、Avatar、Chip、Pagination、Toast 和 HeroUI Pro EmptyState;一级模块桌面间距 32px、移动端 24px,工具栏间距 16px;页面不设置组件背景、颜色、描边、圆角、阴影或状态皮肤。
## Pie / Donut 专用色板
-
Bar、Line、Ranking 和其他普通图表继续使用既有
`--xinyuan-chart-1..16`
、
`--rc-chart-1..16`
及
`--rc-chart-primary/risk/warning/positive/comparison/neutral`
,这些值和语义映射不因 Pie/Donut 改变。
-
Pie/Donut 只使用
`--xinyuan-pie-chart-1..16`
,项目别名为
`--rc-pie-chart-1..16`
。前六色固定为
`#DA241A`
、
`#3F5F86`
、
`#8A668F`
、
`#C28A3D`
、
`#8A6253`
、
`#667080`
;扩展色保持低饱和灰蓝、灰紫、暖棕、暖灰和石板灰。
-
饼图分类色禁止绿色、青绿色、荧光色和彩虹色,也不得复用 HeroUI
`success`
、
`danger`
、
`warning`
、
`foreground`
或
`muted`
状态色。
-
`createResolvedXinyuanPieChartColorMap()`
对完整分类 key 去重、排序、哈希并消解冲突,同一图最多 16 个分类不重复。
`XinyuanEChart`
从专用 Token 解析具体颜色,覆盖 Pie series 与数据项中的旧颜色,但不改变 radius、center、label、labelLine、labelLayout、legend、tooltip 或事件回调。
-
未来新增 Pie/Donut 必须直接使用共享
`XinyuanEChart`
,并通过
`pieCategoryKeys`
传入筛选、排序、分页或 Tab 切换前的完整稳定分类 key 集合;浅色/深色主题共用同一色板,因此分类身份色不随数据顺序、筛选或主题变化。
-
专用色板只匹配 ECharts
`series.type === 'pie'`
,包括实心饼图和通过
`radius`
表示的环形图;不影响页面品牌主色、HeroUI Primary、KPI 或其他图表。
## 跨应用功能范围
## 跨应用功能范围
-
当前应用显示名为“模型分析”,不属于问题整改、审计项目管理、警示案例库、廉洁从业管理、廉洁风险防控、党风廉政建设六个完整功能应用。
-
当前应用显示名为“模型分析”,不属于问题整改、审计项目管理、警示案例库、廉洁从业管理、廉洁风险防控、党风廉政建设六个完整功能应用。
...
...
docs/xinyuan-ui-audit.md
View file @
87f2ef5a
...
@@ -70,11 +70,27 @@
...
@@ -70,11 +70,27 @@
-
Navbar 通知与主题按钮复用批准的共享动作组:36x36px、8px 间距、HeroUI 中性 Surface;移动端 Sidebar Dialog 通过 HeroUI Pro
`Sheet.Heading`
获得可访问名称,复验控制台无警告。
-
Navbar 通知与主题按钮复用批准的共享动作组:36x36px、8px 间距、HeroUI 中性 Surface;移动端 Sidebar Dialog 通过 HeroUI Pro
`Sheet.Heading`
获得可访问名称,复验控制台无警告。
-
当前业务需求没有活动图表;旧 ECharts 页面已移除,因此没有伪造图表或业务数据。
-
当前业务需求没有活动图表;旧 ECharts 页面已移除,因此没有伪造图表或业务数据。
## Pie / Donut 专项审计(2026-09-08)
修改前执行 Pie/Donut、颜色 Token、ECharts 和页面颜色覆盖扫描,活动
`src`
中未发现
`PieChart`
、
`type: 'pie'`
、donut/doughnut、页面级饼图
`itemStyle.color`
或共享 ECharts 适配器调用。
| 页面/路由 | 图表名称 | 原颜色来源 | 目标 Token | 页面级覆盖 | 共享适配器 |
| --- | --- | --- | --- | --- | --- |
| 无 | 当前应用没有活动 Pie/Donut | 不适用 |
`--rc-pie-chart-1..16`
| 无 | 已预置
`XinyuanEChart`
|
-
`theme.css`
仅新增
`--xinyuan-pie-chart-1..16`
,
`tokens.css`
仅新增
`--rc-pie-chart-1..16`
别名;既有普通图表、语义图表、品牌、HeroUI Primary、KPI 和 Surface Token 未修改。
-
前六个专用颜色为
`#DA241A`
、
`#3F5F86`
、
`#8A668F`
、
`#C28A3D`
、
`#8A6253`
、
`#667080`
,后十色延续低饱和灰蓝、灰紫、暖棕、暖灰和石板灰,不包含绿色、青绿色、荧光色或彩虹色。
-
新增
`chartPalette.js`
,通过排序后的稳定业务 key、哈希首选位和顺序冲突消解分配专用色;完整 key 集合不变时,排序和可见数据顺序不会改变颜色,16 个以内不重复。
-
新增业务中立
`XinyuanEChart`
。它只对
`series.type === 'pie'`
覆盖颜色,Pie/Donut 的页面旧颜色无法越过适配器;Bar、Line、Ranking 和其他 series 原样保留。页面未来应传入完整
`pieCategoryKeys`
保障跨筛选和 Tab 的身份稳定。
-
当前没有真实 Pie/Donut 路由或运行数据,因此未制造 2/3/4/6/6+ 分类及 Pie/Donut 浏览器 fixture。专项浏览器数据态验收不适用,待首个真实图表接入时按设计规范补验。
-
专项验证:
`npm run build`
、
`git diff --check`
和共享适配器 esbuild bundle 均通过;16 个稳定分类的映射测试得到
`unique=16`
,反转输入顺序后映射不变。
`npm ls`
确认 HeroUI Pro
`1.0.0-beta.8`
、HeroUI/Styles
`3.2.2`
、React Aria Components
`1.20.0`
、React Aria Utils
`3.34.1`
继续使用当前仓库正式依赖,package/lock 未发生变化。
-
鑫元
`audit_project.sh`
退出码 0。其
`theme.css`
字节比较产生预期 warning:本专项按明确范围只追加 Pie/Donut Token,没有用 skill 快照覆盖目标项目既有普通图表色板;普通
`--xinyuan-chart-*`
和
`--rc-chart-*`
已与修改前
`HEAD`
逐项对比一致。
-
禁色扫描中的
`teal`
/
`emerald`
命中仅属于修改前已有的 KPI tone 名称和业务状态 Chip,不是 Pie/Donut Token、图表数据项颜色或页面级图表覆盖;专用 16 色值无禁用颜色。
## 主题
## 主题
-
`src/design-system/theme.css`
与 Skill 权威文件逐字节一致。
-
`src/design-system/theme.css`
的 HeroUI 核心主题及既有普通图表 Token 未改动,仅追加 Pie/Donut 专用色板。
-
SHA-256:
`a0bf6f243aaf2c94cc8b577145d33d765f8ecab863b3a3b4a358bad4d1a05901`
-
`tokens.css`
的既有别名未改动,仅追加
`--rc-pie-chart-1..16`
。
-
`tokens.css`
与 Skill 副本逐字节一致。
-
Shell gutter:桌面 48px、平板 32px、移动 16px,Navbar 与页面对称共用。
-
Shell gutter:桌面 48px、平板 32px、移动 16px,Navbar 与页面对称共用。
## 浏览器证据
## 浏览器证据
...
...
src/components/XinyuanEChart.jsx
0 → 100644
View file @
87f2ef5a
import
{
createResolvedXinyuanPieChartColorMap
,
getXinyuanEChartsTheme
,
}
from
'@/design-system/chartPalette'
;
import
ReactECharts
from
'echarts-for-react'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useRef
}
from
'react'
;
function
mergeAxisTheme
(
axis
,
theme
)
{
if
(
!
axis
)
return
axis
;
const
applyTheme
=
(
item
)
=>
({
...
item
,
axisLabel
:
{
color
:
theme
.
axis
,
fontFamily
:
theme
.
fontFamily
,
fontSize
:
12
,
...
item
?.
axisLabel
,
},
axisLine
:
{
...
item
?.
axisLine
,
lineStyle
:
{
color
:
theme
.
grid
,
...
item
?.
axisLine
?.
lineStyle
},
},
axisTick
:
{
...
item
?.
axisTick
,
lineStyle
:
{
color
:
theme
.
grid
,
...
item
?.
axisTick
?.
lineStyle
},
},
splitLine
:
{
...
item
?.
splitLine
,
lineStyle
:
{
color
:
theme
.
grid
,
...
item
?.
splitLine
?.
lineStyle
},
},
});
return
Array
.
isArray
(
axis
)
?
axis
.
map
(
applyTheme
)
:
applyTheme
(
axis
);
}
function
forceItemStyle
(
data
,
itemStyle
)
{
if
(
!
Array
.
isArray
(
data
))
return
data
;
return
data
.
map
((
item
)
=>
{
if
(
!
item
||
typeof
item
!==
'object'
||
Array
.
isArray
(
item
))
return
item
;
return
{
...
item
,
itemStyle
:
{
...
item
.
itemStyle
,
...
itemStyle
}
};
});
}
function
readLegendCategoryKeys
(
legend
)
{
const
legends
=
Array
.
isArray
(
legend
)
?
legend
:
legend
?
[
legend
]
:
[];
return
legends
.
flatMap
((
item
)
=>
(
Array
.
isArray
(
item
?.
data
)
?
item
.
data
.
map
((
entry
)
=>
(
typeof
entry
===
'object'
?
entry
?.
name
:
entry
))
:
[]
));
}
export
default
function
XinyuanEChart
({
ariaLabel
,
className
=
''
,
height
=
320
,
onEvents
,
option
,
pieCategoryKeys
=
[],
})
{
const
chartRef
=
useRef
(
null
);
const
hideTooltip
=
useCallback
(()
=>
{
chartRef
.
current
?.
getEchartsInstance
?.().
dispatchAction
({
type
:
'hideTip'
});
},
[]);
useEffect
(()
=>
{
window
.
addEventListener
(
'resize'
,
hideTooltip
);
return
()
=>
window
.
removeEventListener
(
'resize'
,
hideTooltip
);
},
[
hideTooltip
]);
const
themedOption
=
useMemo
(()
=>
{
const
theme
=
getXinyuanEChartsTheme
();
const
sourceSeries
=
Array
.
isArray
(
option
?.
series
)
?
option
.
series
:
[
option
?.
series
];
const
completePieCategoryKeys
=
[
...
pieCategoryKeys
,
...
readLegendCategoryKeys
(
option
?.
legend
),
...
sourceSeries
.
flatMap
((
series
)
=>
(
series
?.
type
===
'pie'
&&
Array
.
isArray
(
series
.
data
)
?
series
.
data
.
map
((
item
)
=>
item
?.
name
)
:
[]
)),
];
const
pieColorMap
=
createResolvedXinyuanPieChartColorMap
(
completePieCategoryKeys
);
const
applyPieTheme
=
(
series
)
=>
{
if
(
series
?.
type
!==
'pie'
)
return
series
;
const
seriesItemStyle
=
{
...
series
.
itemStyle
};
delete
seriesItemStyle
.
color
;
const
pieData
=
Array
.
isArray
(
series
.
data
)
?
series
.
data
.
map
((
item
,
index
)
=>
{
if
(
!
item
||
typeof
item
!==
'object'
||
Array
.
isArray
(
item
))
return
item
;
const
key
=
String
(
item
.
name
??
''
).
trim
();
return
{
...
item
,
itemStyle
:
{
...
item
.
itemStyle
,
color
:
pieColorMap
.
get
(
key
)
||
theme
.
piePalette
[
index
%
theme
.
piePalette
.
length
],
},
};
})
:
series
.
data
;
return
{
...
series
,
color
:
theme
.
piePalette
,
itemStyle
:
{
...
seriesItemStyle
,
borderColor
:
theme
.
sectorGap
,
borderRadius
:
theme
.
sectorRadius
,
borderWidth
:
theme
.
sectorGapWidth
,
},
data
:
forceItemStyle
(
pieData
,
{
borderColor
:
theme
.
sectorGap
,
borderRadius
:
theme
.
sectorRadius
,
borderWidth
:
theme
.
sectorGapWidth
,
}),
padAngle
:
1
,
};
};
const
themedSeries
=
Array
.
isArray
(
option
?.
series
)
?
option
.
series
.
map
(
applyPieTheme
)
:
applyPieTheme
(
option
?.
series
);
return
{
animationDuration
:
320
,
animationDurationUpdate
:
240
,
animationEasing
:
'cubicOut'
,
aria
:
{
enabled
:
true
,
decal
:
{
show
:
false
},
...
option
?.
aria
},
color
:
option
?.
color
||
theme
.
palette
,
textStyle
:
{
color
:
theme
.
label
,
fontFamily
:
theme
.
fontFamily
,
...
option
?.
textStyle
,
},
...
option
,
series
:
themedSeries
,
legend
:
Array
.
isArray
(
option
?.
legend
)
?
option
.
legend
.
map
((
legend
)
=>
({
icon
:
'circle'
,
itemHeight
:
8
,
itemWidth
:
8
,
...
legend
,
textStyle
:
{
color
:
theme
.
label
,
fontFamily
:
theme
.
fontFamily
,
fontSize
:
12
,
lineHeight
:
16
,
...
legend
?.
textStyle
,
},
}))
:
option
?.
legend
?
{
icon
:
'circle'
,
itemHeight
:
8
,
itemWidth
:
8
,
...
option
.
legend
,
textStyle
:
{
color
:
theme
.
label
,
fontFamily
:
theme
.
fontFamily
,
fontSize
:
12
,
lineHeight
:
16
,
...
option
.
legend
?.
textStyle
,
},
}
:
undefined
,
tooltip
:
{
appendToBody
:
true
,
backgroundColor
:
theme
.
tooltipBackground
,
borderColor
:
theme
.
tooltipBorder
,
borderWidth
:
1
,
className
:
'rc-echarts-tooltip'
,
confine
:
true
,
enterable
:
false
,
extraCssText
:
`border-radius:
${
theme
.
tooltipRadius
}
; box-shadow:
${
theme
.
tooltipShadow
}
;`
,
padding
:
[
10
,
12
],
renderMode
:
'html'
,
...
option
?.
tooltip
,
textStyle
:
{
color
:
theme
.
foreground
,
fontFamily
:
theme
.
fontFamily
,
fontSize
:
12
,
...
option
?.
tooltip
?.
textStyle
,
},
},
xAxis
:
mergeAxisTheme
(
option
?.
xAxis
,
theme
),
yAxis
:
mergeAxisTheme
(
option
?.
yAxis
,
theme
),
};
},
[
option
,
pieCategoryKeys
]);
return
(
<
div
aria
-
label=
{
ariaLabel
}
className=
{
`rc-chart-interaction w-full min-w-0 ${className}`
.
trim
()
}
role=
"img"
tabIndex=
{
0
}
>
<
ReactECharts
lazyUpdate
notMerge
onEvents=
{
{
globalout
:
hideTooltip
,
...
onEvents
}
}
option=
{
themedOption
}
opts=
{
{
renderer
:
'canvas'
}
}
ref=
{
chartRef
}
style=
{
{
height
,
width
:
'100%'
}
}
/>
</
div
>
);
}
src/design-system/chartPalette.js
0 → 100644
View file @
87f2ef5a
export
const
XINYUAN_CHART_PALETTE
=
Object
.
freeze
(
Array
.
from
({
length
:
16
},
(
_
,
index
)
=>
`var(--rc-chart-
${
index
+
1
}
)`
),
);
export
const
XINYUAN_PIE_CHART_PALETTE
=
Object
.
freeze
(
Array
.
from
({
length
:
16
},
(
_
,
index
)
=>
`var(--rc-pie-chart-
${
index
+
1
}
)`
),
);
export
const
XINYUAN_CHART_SEMANTIC
=
Object
.
freeze
({
primary
:
'var(--rc-chart-primary)'
,
risk
:
'var(--rc-chart-risk)'
,
warning
:
'var(--rc-chart-warning)'
,
positive
:
'var(--rc-chart-positive)'
,
comparison
:
'var(--rc-chart-comparison)'
,
neutral
:
'var(--rc-chart-neutral)'
,
});
function
normalizeChartKey
(
key
)
{
return
String
(
key
??
''
).
trim
();
}
function
hashChartKey
(
key
)
{
return
Array
.
from
(
key
).
reduce
(
(
value
,
character
)
=>
((
value
*
31
)
+
character
.
codePointAt
(
0
))
>>>
0
,
0
,
);
}
export
function
getXinyuanChartColor
(
key
,
fallbackIndex
=
0
)
{
const
stableKey
=
normalizeChartKey
(
key
);
if
(
!
stableKey
)
{
return
XINYUAN_CHART_PALETTE
[
fallbackIndex
%
XINYUAN_CHART_PALETTE
.
length
];
}
return
XINYUAN_CHART_PALETTE
[
hashChartKey
(
stableKey
)
%
XINYUAN_CHART_PALETTE
.
length
];
}
export
function
getXinyuanPieChartColor
(
index
=
0
)
{
return
XINYUAN_PIE_CHART_PALETTE
[
Math
.
max
(
0
,
Number
(
index
)
||
0
)
%
XINYUAN_PIE_CHART_PALETTE
.
length
];
}
export
function
createXinyuanChartColorMap
(
keys
=
[])
{
const
stableKeys
=
[...
new
Set
(
keys
.
map
(
normalizeChartKey
).
filter
(
Boolean
))].
sort
();
const
colorMap
=
new
Map
();
const
usedIndexes
=
new
Set
();
stableKeys
.
forEach
((
key
,
keyIndex
)
=>
{
const
preferredIndex
=
hashChartKey
(
key
)
%
XINYUAN_CHART_PALETTE
.
length
;
let
paletteIndex
=
preferredIndex
;
if
(
keyIndex
<
XINYUAN_CHART_PALETTE
.
length
)
{
for
(
let
offset
=
0
;
offset
<
XINYUAN_CHART_PALETTE
.
length
;
offset
+=
1
)
{
const
candidateIndex
=
(
preferredIndex
+
offset
)
%
XINYUAN_CHART_PALETTE
.
length
;
if
(
!
usedIndexes
.
has
(
candidateIndex
))
{
paletteIndex
=
candidateIndex
;
usedIndexes
.
add
(
candidateIndex
);
break
;
}
}
}
colorMap
.
set
(
key
,
XINYUAN_CHART_PALETTE
[
paletteIndex
]);
});
return
colorMap
;
}
function
readRootToken
(
tokenName
,
fallback
=
''
)
{
if
(
typeof
window
===
'undefined'
||
typeof
document
===
'undefined'
)
return
fallback
;
return
window
.
getComputedStyle
(
document
.
documentElement
)
.
getPropertyValue
(
tokenName
)
.
trim
()
||
fallback
;
}
const
concreteColorCache
=
new
Map
();
function
convertOklchToRgb
(
value
)
{
const
match
=
String
(
value
).
trim
().
match
(
/^oklch
\(\s
*
([\d
.
]
+
)
%
?\s
+
([\d
.
]
+
)
%
?\s
+
([\d
.-
]
+
)(?:
deg
)?\s
*
\)
$/i
,
);
if
(
!
match
)
return
value
;
const
lightnessValue
=
Number
(
match
[
1
]);
const
chromaValue
=
Number
(
match
[
2
]);
const
lightness
=
lightnessValue
>
1
?
lightnessValue
/
100
:
lightnessValue
;
const
chroma
=
chromaValue
>
1
?
chromaValue
/
100
:
chromaValue
;
const
hue
=
(
Number
(
match
[
3
])
*
Math
.
PI
)
/
180
;
const
a
=
chroma
*
Math
.
cos
(
hue
);
const
b
=
chroma
*
Math
.
sin
(
hue
);
const
l
=
(
lightness
+
0.3963377774
*
a
+
0.2158037573
*
b
)
**
3
;
const
m
=
(
lightness
-
0.1055613458
*
a
-
0.0638541728
*
b
)
**
3
;
const
s
=
(
lightness
-
0.0894841775
*
a
-
1.291485548
*
b
)
**
3
;
const
channels
=
[
4.0767416621
*
l
-
3.3077115913
*
m
+
0.2309699292
*
s
,
-
1.2684380046
*
l
+
2.6097574011
*
m
-
0.3413193965
*
s
,
-
0.0041960863
*
l
-
0.7034186147
*
m
+
1.707614701
*
s
,
].
map
((
channel
)
=>
{
const
clamped
=
Math
.
max
(
0
,
Math
.
min
(
1
,
channel
));
const
encoded
=
clamped
<=
0.0031308
?
12.92
*
clamped
:
1.055
*
(
clamped
**
(
1
/
2.4
))
-
0.055
;
return
Math
.
round
(
encoded
*
255
);
});
return
`rgb(
${
channels
.
join
(
', '
)}
)`
;
}
function
readConcreteColorToken
(
tokenName
,
fallback
=
''
)
{
const
raw
=
readRootToken
(
tokenName
,
fallback
);
const
cached
=
concreteColorCache
.
get
(
tokenName
);
if
(
cached
?.
raw
===
raw
)
return
cached
.
value
;
if
(
typeof
document
===
'undefined'
||
!
raw
)
return
raw
||
fallback
;
const
probe
=
document
.
createElement
(
'span'
);
probe
.
style
.
color
=
raw
;
if
(
!
probe
.
style
.
color
)
return
fallback
;
probe
.
style
.
position
=
'absolute'
;
probe
.
style
.
pointerEvents
=
'none'
;
document
.
body
?.
appendChild
(
probe
);
const
resolved
=
convertOklchToRgb
(
window
.
getComputedStyle
(
probe
).
color
||
fallback
);
probe
.
remove
();
concreteColorCache
.
set
(
tokenName
,
{
raw
,
value
:
resolved
});
return
resolved
;
}
export
function
resolveXinyuanChartColor
(
color
,
fallback
=
''
)
{
const
match
=
String
(
color
||
''
).
trim
().
match
(
/^var
\((
--
[^
)
]
+
)\)
$/
);
return
match
?
readConcreteColorToken
(
match
[
1
],
fallback
)
:
String
(
color
||
fallback
);
}
export
function
createResolvedXinyuanChartColorMap
(
keys
=
[])
{
return
new
Map
(
Array
.
from
(
createXinyuanChartColorMap
(
keys
)).
map
(([
key
,
color
])
=>
[
key
,
resolveXinyuanChartColor
(
color
),
]),
);
}
export
function
createXinyuanPieChartColorMap
(
keys
=
[])
{
const
stableKeys
=
[...
new
Set
(
keys
.
map
(
normalizeChartKey
).
filter
(
Boolean
))].
sort
();
const
colorMap
=
new
Map
();
const
usedIndexes
=
new
Set
();
stableKeys
.
forEach
((
key
)
=>
{
const
preferredIndex
=
hashChartKey
(
key
)
%
XINYUAN_PIE_CHART_PALETTE
.
length
;
let
paletteIndex
=
preferredIndex
;
for
(
let
offset
=
0
;
offset
<
XINYUAN_PIE_CHART_PALETTE
.
length
;
offset
+=
1
)
{
const
candidate
=
(
preferredIndex
+
offset
)
%
XINYUAN_PIE_CHART_PALETTE
.
length
;
if
(
!
usedIndexes
.
has
(
candidate
))
{
paletteIndex
=
candidate
;
usedIndexes
.
add
(
candidate
);
break
;
}
}
colorMap
.
set
(
key
,
getXinyuanPieChartColor
(
paletteIndex
));
});
return
colorMap
;
}
export
function
createResolvedXinyuanPieChartColorMap
(
keys
=
[])
{
return
new
Map
(
Array
.
from
(
createXinyuanPieChartColorMap
(
keys
)).
map
(([
key
,
color
])
=>
[
key
,
resolveXinyuanChartColor
(
color
),
]),
);
}
export
function
getXinyuanEChartsTheme
()
{
const
documentFont
=
typeof
document
===
'undefined'
?
'Inter, sans-serif'
:
window
.
getComputedStyle
(
document
.
body
).
fontFamily
;
return
{
axis
:
readRootToken
(
'--xinyuan-chart-axis'
,
'#737373'
),
fontFamily
:
documentFont
||
'Inter, sans-serif'
,
foreground
:
readConcreteColorToken
(
'--foreground'
,
readRootToken
(
'--xinyuan-chart-tooltip-foreground'
,
'#222222'
),
),
grid
:
readRootToken
(
'--xinyuan-chart-grid'
,
'#e8e8e8'
),
label
:
readRootToken
(
'--xinyuan-chart-label'
,
'#737373'
),
palette
:
Array
.
from
({
length
:
16
},
(
_
,
index
)
=>
(
readRootToken
(
`--xinyuan-chart-
${
index
+
1
}
`
,
'#667085'
)
)),
piePalette
:
Array
.
from
({
length
:
16
},
(
_
,
index
)
=>
(
readConcreteColorToken
(
`--rc-pie-chart-
${
index
+
1
}
`
,
'#667080'
)
)),
sectorGap
:
readConcreteColorToken
(
'--xinyuan-chart-sector-gap'
,
'#ffffff'
),
sectorGapWidth
:
Number
.
parseFloat
(
readRootToken
(
'--xinyuan-chart-sector-gap-width'
,
'2'
),
)
||
2
,
sectorRadius
:
Number
.
parseFloat
(
readRootToken
(
'--xinyuan-chart-sector-radius'
,
'10'
),
)
||
10
,
tooltipRadius
:
readRootToken
(
'--xinyuan-chart-tooltip-radius'
,
'0.75rem'
),
tooltipBackground
:
readRootToken
(
'--xinyuan-chart-tooltip-background'
,
'#ffffff'
),
tooltipBorder
:
readRootToken
(
'--xinyuan-chart-tooltip-border'
,
'#dedede'
),
tooltipShadow
:
readRootToken
(
'--xinyuan-chart-tooltip-shadow'
,
'0 16px 40px rgb(15 23 42 / 14%)'
,
),
};
}
src/design-system/theme.css
View file @
87f2ef5a
...
@@ -106,6 +106,23 @@
...
@@ -106,6 +106,23 @@
--xinyuan-chart-14
:
#8A6F63
;
--xinyuan-chart-14
:
#8A6F63
;
--xinyuan-chart-15
:
#9A8174
;
--xinyuan-chart-15
:
#9A8174
;
--xinyuan-chart-16
:
#B0A5AD
;
--xinyuan-chart-16
:
#B0A5AD
;
/* Pie / donut only. Keep this independent from the categorical palette above. */
--xinyuan-pie-chart-1
:
#DA241A
;
--xinyuan-pie-chart-2
:
#3F5F86
;
--xinyuan-pie-chart-3
:
#8A668F
;
--xinyuan-pie-chart-4
:
#C28A3D
;
--xinyuan-pie-chart-5
:
#8A6253
;
--xinyuan-pie-chart-6
:
#667080
;
--xinyuan-pie-chart-7
:
#74879A
;
--xinyuan-pie-chart-8
:
#9A8174
;
--xinyuan-pie-chart-9
:
#6F6674
;
--xinyuan-pie-chart-10
:
#B87958
;
--xinyuan-pie-chart-11
:
#61758F
;
--xinyuan-pie-chart-12
:
#807783
;
--xinyuan-pie-chart-13
:
#B0A4A0
;
--xinyuan-pie-chart-14
:
#AFA5B1
;
--xinyuan-pie-chart-15
:
#897A73
;
--xinyuan-pie-chart-16
:
#556B88
;
--xinyuan-chart-sector-gap
:
var
(
--surface
);
--xinyuan-chart-sector-gap
:
var
(
--surface
);
--xinyuan-chart-sector-radius
:
10px
;
--xinyuan-chart-sector-radius
:
10px
;
--xinyuan-chart-grid
:
#e8e8e8
;
--xinyuan-chart-grid
:
#e8e8e8
;
...
...
src/design-system/tokens.css
View file @
87f2ef5a
...
@@ -168,6 +168,23 @@
...
@@ -168,6 +168,23 @@
--rc-chart-14
:
var
(
--xinyuan-chart-14
);
--rc-chart-14
:
var
(
--xinyuan-chart-14
);
--rc-chart-15
:
var
(
--xinyuan-chart-15
);
--rc-chart-15
:
var
(
--xinyuan-chart-15
);
--rc-chart-16
:
var
(
--xinyuan-chart-16
);
--rc-chart-16
:
var
(
--xinyuan-chart-16
);
/* Pie / donut aliases never feed bar, line, ranking or KPI colors. */
--rc-pie-chart-1
:
var
(
--xinyuan-pie-chart-1
);
--rc-pie-chart-2
:
var
(
--xinyuan-pie-chart-2
);
--rc-pie-chart-3
:
var
(
--xinyuan-pie-chart-3
);
--rc-pie-chart-4
:
var
(
--xinyuan-pie-chart-4
);
--rc-pie-chart-5
:
var
(
--xinyuan-pie-chart-5
);
--rc-pie-chart-6
:
var
(
--xinyuan-pie-chart-6
);
--rc-pie-chart-7
:
var
(
--xinyuan-pie-chart-7
);
--rc-pie-chart-8
:
var
(
--xinyuan-pie-chart-8
);
--rc-pie-chart-9
:
var
(
--xinyuan-pie-chart-9
);
--rc-pie-chart-10
:
var
(
--xinyuan-pie-chart-10
);
--rc-pie-chart-11
:
var
(
--xinyuan-pie-chart-11
);
--rc-pie-chart-12
:
var
(
--xinyuan-pie-chart-12
);
--rc-pie-chart-13
:
var
(
--xinyuan-pie-chart-13
);
--rc-pie-chart-14
:
var
(
--xinyuan-pie-chart-14
);
--rc-pie-chart-15
:
var
(
--xinyuan-pie-chart-15
);
--rc-pie-chart-16
:
var
(
--xinyuan-pie-chart-16
);
--rc-chart-primary
:
var
(
--rc-chart-1
);
--rc-chart-primary
:
var
(
--rc-chart-1
);
--rc-chart-risk
:
var
(
--rc-chart-1
);
--rc-chart-risk
:
var
(
--rc-chart-1
);
--rc-chart-warning
:
var
(
--rc-chart-3
);
--rc-chart-warning
:
var
(
--rc-chart-3
);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment