Commit b20234c0 authored by Andy-bubu's avatar Andy-bubu

feat: add xinyuan pie chart palette

parent d0a2a6ab
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
- 源码中存在蓝、sky、rose、pink、渐变和页面私有品牌色;专项优化后主色统一为 `#DA241A`,历史问题记录保留用于追踪。 - 源码中存在蓝、sky、rose、pink、渐变和页面私有品牌色;专项优化后主色统一为 `#DA241A`,历史问题记录保留用于追踪。
- 同时存在 Heroicons 和局部自绘图标,缺少 Ant Design Icons 适配层。 - 同时存在 Heroicons 和局部自绘图标,缺少 Ant Design Icons 适配层。
- 没有根级暗色主题,Portal 中的 Dropdown、Popover、Modal、Tooltip 和上传面板无法保证一致暗色表面。 - 没有根级暗色主题,Portal 中的 Dropdown、Popover、Modal、Tooltip 和上传面板无法保证一致暗色表面。
- 当前活动路由未渲染数据图表;图表规范作为后续功能的强制合同写入设计文档。 - 当前活动路由未渲染数据图表;图表规范作为后续功能的强制合同写入设计文档。共享 `XinyuanEChart` 已预留 Pie/Donut 专用适配,仅允许通过 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*` 取色。
## 4. 迁移边界 ## 4. 迁移边界
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
- 多选动作仅显示在当前视口底部居中的 ActionBar,右侧使用关闭图标清除选择。 - 多选动作仅显示在当前视口底部居中的 ActionBar,右侧使用关闭图标清除选择。
- 业务列表沿用各模块既有分页口径;通知列表固定每页 10 条且不显示页容量选择器。Pagination 距离 DataGrid 约 8px。 - 业务列表沿用各模块既有分页口径;通知列表固定每页 10 条且不显示页容量选择器。Pagination 距离 DataGrid 约 8px。
- 成功反馈使用顶部居中的 3-4 秒 Toast,不得用页面流 Alert 推挤筛选或表格。 - 成功反馈使用顶部居中的 3-4 秒 Toast,不得用页面流 Alert 推挤筛选或表格。
- 图表和 KPI 使用独立的低饱和色板:品牌红、暖棕、灰紫、蓝灰依次承载系列,不使用绿色、荧光色或彩虹式相邻颜色;单系列固定使用 `#DA241A`。 - 普通柱状图、折线图、排名图和组合图继续使用 `--xinyuan-chart-*` / `--rc-chart-*` 低饱和色板;单系列固定使用 `#DA241A`。饼图/环形图是独立例外,仅由共享 `XinyuanEChart` 适配器使用 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*`:前六色依次为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,扩展色限定为灰蓝、灰紫、暖棕、暖灰和石板灰;禁止绿色、青绿色、荧光色和彩虹色。颜色按完整稳定分类 key 确定性分配并消除冲突,筛选、排序、Tab 切换和主题变化不得改变分类颜色,也不影响页面主色或普通图表色板。
## 6. 表单、上传与弹窗 ## 6. 表单、上传与弹窗
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
- KPI 网格移动端 1 列、中屏 2 列;桌面 4 张为 4 列、5 张为 3+2、6 张为 3+3、8 张为 4+4。 - KPI 网格移动端 1 列、中屏 2 列;桌面 4 张为 4 列、5 张为 3+2、6 张为 3+3、8 张为 4+4。
- KPI Card 使用不透明中性表面、24px 圆角、细边框与共享轻阴影;不得使用渐变或每卡异色背景。 - KPI Card 使用不透明中性表面、24px 圆角、细边框与共享轻阴影;不得使用渐变或每卡异色背景。
- 密集 KPI 图标面为 48px、16px 圆角、20px Ant 图标,容器与 SVG 中心偏差不得超过 0.5px。 - 密集 KPI 图标面为 48px、16px 圆角、20px Ant 图标,容器与 SVG 中心偏差不得超过 0.5px。
- ECharts 必须从共享主题 Hook 读取浅色/暗色文字、网格、系列和 Tooltip Token。 - ECharts 必须从共享 `XinyuanEChart` / `chartPalette.js` 读取浅色/暗色文字、网格、系列和 Tooltip Token;未来新增饼图/环形图不得在页面直接写 `itemStyle.color`,必须使用专用 Pie/Donut Token。
- 图表 Tooltip 使用根主题 overlay surface,不能成为白色孤岛,也不能遮挡关键数据。 - 图表 Tooltip 使用根主题 overlay surface,不能成为白色孤岛,也不能遮挡关键数据。
## 8. 响应式与交付检查 ## 8. 响应式与交付检查
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
- 通知:页面直接组合 HeroUI Tabs/SearchField/Card/Chip/Badge/Tooltip 与 Pro EmptyState;业务 service 复用 `DataService` 的 `/notice/customer/*` 接口,Shell 只持有真实未读计数和 60 秒刷新事件,不在 Sidebar 建立第二入口。 - 通知:页面直接组合 HeroUI Tabs/SearchField/Card/Chip/Badge/Tooltip 与 Pro EmptyState;业务 service 复用 `DataService` 的 `/notice/customer/*` 接口,Shell 只持有真实未读计数和 60 秒刷新事件,不在 Sidebar 建立第二入口。
- 应用切换:共享 `ApplicationSwitcherDialog` 直接组合 HeroUI Modal、Pro ItemCardGroup/ItemCard、Surface/Avatar/Chip 与 Pro EmptyState;Modal 仅保留无障碍名称和搜索字段,不显示重复标题/关闭图标,面板使用页面 `background`。`SiteContext.siteInfo.group` 只负责动态分组与顺序并在跨组时去重,`UserContext.applications` 和 `/application/info` 继续负责访问集合与详情。授权域 `audit_manager` 规范化为唯一当前项,并导航到部署基路径 `/audit_manager_new/`。 - 应用切换:共享 `ApplicationSwitcherDialog` 直接组合 HeroUI Modal、Pro ItemCardGroup/ItemCard、Surface/Avatar/Chip 与 Pro EmptyState;Modal 仅保留无障碍名称和搜索字段,不显示重复标题/关闭图标,面板使用页面 `background`。`SiteContext.siteInfo.group` 只负责动态分组与顺序并在跨组时去重,`UserContext.applications` 和 `/application/info` 继续负责访问集合与详情。授权域 `audit_manager` 规范化为唯一当前项,并导航到部署基路径 `/audit_manager_new/`。
- 上传:Pro DropZone,页面边界接收真实 File。 - 上传:Pro DropZone,页面边界接收真实 File。
- KPI/图表:仅业务存在时使用 Pro KPI/PieChart/BarChart/ComposedChart/ChartTooltip;当前活动路由无真实图表,后续图表必须使用 `--xinyuan-chart-*` 低饱和 token。 - KPI/图表:仅业务存在时使用 Pro KPI 与 HeroUI 图表外壳;实际 ECharts 由共享 `XinyuanEChart` 适配器统一承载,当前活动路由无真实图表。普通柱状图、折线图和组合图必须使用 `--xinyuan-chart-*`,饼图/环形图必须使用独立 `--xinyuan-pie-chart-*`,不得在页面写入颜色常量。
- 报告视图:一级 HeroUI Tabs 切换 current/history,二级内容宽度 Tabs 以稳定项目 ID 切换报告快照;当前项目只挂载工具栏和 DataGrid,不重复 Tabs 已表达的项目标题,整体进度由 KPI 承担。项目与控制点仍携带原数组索引进入现有更新/确认回调。 - 报告视图:一级 HeroUI Tabs 切换 current/history,二级内容宽度 Tabs 以稳定项目 ID 切换报告快照;当前项目只挂载工具栏和 DataGrid,不重复 Tabs 已表达的项目标题,整体进度由 KPI 承担。项目与控制点仍携带原数组索引进入现有更新/确认回调。
- 当前报告搜索由 `AuditReportContent` 放在二级 Tabs 下方,并与当前项目确认、备注保存组成标准响应式列表工具栏;它继续使用父页面原受控 keyword 与保存回调。 - 当前报告搜索由 `AuditReportContent` 放在二级 Tabs 下方,并与当前项目确认、备注保存组成标准响应式列表工具栏;它继续使用父页面原受控 keyword 与保存回调。
- 报告附件单元格由 HeroUI `Chip/Link` 与 Pro `DropZone` 直接组合,不创建 Card/Surface 包装层;上传、追加和删除仍进入原 `onUpdate(processIndex, pointIndex, patch)` 协议。 - 报告附件单元格由 HeroUI `Chip/Link` 与 Pro `DropZone` 直接组合,不创建 Card/Surface 包装层;上传、追加和删除仍进入原 `onUpdate(processIndex, pointIndex, patch)` 协议。
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
| 报告附件单元格 | 报告控制点 DataGrid | 完成 | 缺有效登录态/真实附件 | HeroUI Chip/Link;无嵌套 Card | | 报告附件单元格 | 报告控制点 DataGrid | 完成 | 缺有效登录态/真实附件 | HeroUI Chip/Link;无嵌套 Card |
| 历史详情 | `/audit_report/:reportId` | 完成 | 缺有效登录态/真实记录 | 代码复核完成 | | 历史详情 | `/audit_report/:reportId` | 完成 | 缺有效登录态/真实记录 | 代码复核完成 |
| 外部填报 | `/audit_report_fill` | 完成 | 缺真实 ticket/report | 代码复核完成 | | 外部填报 | `/audit_report_fill` | 完成 | 缺真实 ticket/report | 代码复核完成 |
| KPI/图表 | 报告 KPI;无图表 | KPI 完成/图表不适用 | 缺有效登录态 | 不制造数据 | | KPI/图表 | 报告 KPI;当前无真实图表调用 | KPI 完成;共享 ECharts 适配器与 Pie/Donut 专用色板已就绪 | 无真实图表,保持不适用 | 不制造数据;未来饼图/环图按稳定 key 使用专用 Token |
## 最终门槛 ## 最终门槛
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
`src/pages/process_monitor`、`src/pages/RecordDetailDrawer`、`src/components/record-workbench` 不在当前路由树;排除 Catalyst Kit 自身目录后,共有 7 个存量源码文件仍含 Catalyst/Headless 引用。不能把这些目录误报为已迁移。 `src/pages/process_monitor`、`src/pages/RecordDetailDrawer`、`src/components/record-workbench` 不在当前路由树;排除 Catalyst Kit 自身目录后,共有 7 个存量源码文件仍含 Catalyst/Headless 引用。不能把这些目录误报为已迁移。
# 本轮迁移清单(2026-09-02) # 本轮迁移清单(2026-09-02)
- [x] 权威 theme.css / tokens.css / shell.css 与审计项目管理同步 - [x] 权威 theme.css / tokens.css / shell.css 与审计项目管理同步;Pie/Donut 专用 Token 与共享适配器已同步
- [x] 固定深色 Sidebar、浅色内容区、HeroUI Pro Navbar.Trigger、应用切换 Modal - [x] 固定深色 Sidebar、浅色内容区、HeroUI Pro Navbar.Trigger、应用切换 Modal
- [x] 共享 DataTable 增强固定列、sticky/横向滚动、EmptyState、ActionBar、20/50/100 页容量接口 - [x] 共享 DataTable 增强固定列、sticky/横向滚动、EmptyState、ActionBar、20/50/100 页容量接口
- [x] `process_monitor` 页面移除 Catalyst、Headless UI、Heroicons 和私有渐变卡片表格 - [x] `process_monitor` 页面移除 Catalyst、Headless UI、Heroicons 和私有渐变卡片表格
......
...@@ -61,9 +61,10 @@ ...@@ -61,9 +61,10 @@
## KPI 与图表 ## KPI 与图表
- 业务存在数值 KPI 时必须使用 Pro KPI compound API;模块 24px radius、16px grid gap、业务图标和唯一 tone。 - 业务存在数值 KPI 时必须使用 Pro KPI compound API;模块 24px radius、16px grid gap、业务图标和唯一 tone。
- 业务存在图表时使用 Pro PieChart、BarChart、ComposedChart 与 ChartTooltipContent,并遵循共享 chart Tokens。 - 业务存在图表时,HeroUI Pro 负责图表容器与交互外壳;实际图表统一通过共享 `XinyuanEChart`(`echarts-for-react`)适配器渲染,并遵循共享 chart Tokens。页面不得直接创建私有图表主题或颜色常量。
- 稽核报告的现有业务统计使用 Pro KPI compound API;活动路由没有图表,不新增演示数字或伪造业务数据。 - 稽核报告的现有业务统计使用 Pro KPI compound API;活动路由没有图表,不新增演示数字或伪造业务数据。
- 图表色板独立于 UI 状态色,按“品牌红 -> 暖色 -> 中性灰 -> 蓝灰”顺序使用 `--xinyuan-chart-1..16`;单系列统一 `#DA241A`,多系列按稳定业务键分配相近低饱和色,不使用绿色、荧光色或彩虹序列。KPI 图标 tone 复用同一色板,避免蓝/绿/紫高对比跳色。当前活动路由没有真实图表调用,因此只维护 token 合同,不添加伪造图表或数据。 - 图表色板独立于 UI 状态色,普通柱状图、折线图、排名图和组合图继续使用 `--xinyuan-chart-1..16` / `--rc-chart-*`;单系列统一 `#DA241A`,多系列按稳定业务键分配相近低饱和色,不使用绿色、荧光色或彩虹序列。当前活动路由没有真实图表调用,因此只维护共享适配器和 Token 合同,不添加伪造图表或数据。
- 饼图 / 环形图是普通分类色板之外的专用例外,仅由共享 `XinyuanEChart` 在 `series.type === 'pie'` 时接管,使用 `--xinyuan-pie-chart-1..16` / `--rc-pie-chart-*`;前六个固定为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,后续使用低饱和灰蓝、灰紫、暖棕、暖灰和石板灰扩展。饼图禁止绿色、青绿色、荧光色和彩虹色;按完整且排序后的稳定分类 key 确定性分配并消除冲突,筛选、排序、Tab 切换和主题变化不得改变分类颜色。该色板不影响页面品牌主色及普通图表 Token。
## 验收 ## 验收
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
- 活动弹窗使用 HeroUI Modal/AlertDialog;普通辅助按钮使用 `secondary`,删除确认使用 danger。控制点查看态采用无框单列字段布局,字段标签使用 muted、字段值使用 foreground,避免在 Overlay 上叠加同层级 Surface;控制点查看和编辑在同一 Modal 内切换,编辑入口保留权限与原保存回调。 - 活动弹窗使用 HeroUI Modal/AlertDialog;普通辅助按钮使用 `secondary`,删除确认使用 danger。控制点查看态采用无框单列字段布局,字段标签使用 muted、字段值使用 foreground,避免在 Overlay 上叠加同层级 Surface;控制点查看和编辑在同一 Modal 内切换,编辑入口保留权限与原保存回调。
- 全应用弹窗已统一到鑫元尺寸阶梯:确认弹窗 `md/480px`,单列表单与填报链接 `lg/640px`,控制点详情/表单使用 `4xl/1088px`;应用切换保持基准 `46rem/736px`。大尺寸面板通过 `bg-background p-7 sm:max-w-[68rem]` 与 HeroUI `Modal.Container size="lg"` 组合实现,正文保持独立滚动。 - 全应用弹窗已统一到鑫元尺寸阶梯:确认弹窗 `md/480px`,单列表单与填报链接 `lg/640px`,控制点详情/表单使用 `4xl/1088px`;应用切换保持基准 `46rem/736px`。大尺寸面板通过 `bg-background p-7 sm:max-w-[68rem]` 与 HeroUI `Modal.Container size="lg"` 组合实现,正文保持独立滚动。
- 报告页已有真实业务统计改为 Pro KPI compound API;活动路由没有图表,不新增图表或演示数据。主题专项已固定 HeroUI `--accent`/`--focus` 为 `#DA241A`,并提供完整 `--primary-50..950` 与 `--color-primary-*` 色阶;success 映射为低饱和蓝灰,图表 token 不含绿色。 - 报告页已有真实业务统计改为 Pro KPI compound API;活动路由没有图表,不新增图表或演示数据。主题专项已固定 HeroUI `--accent`/`--focus` 为 `#DA241A`,并提供完整 `--primary-50..950` 与 `--color-primary-*` 色阶;success 映射为低饱和蓝灰,图表 token 不含绿色。
- 全仓库无 ECharts 源码引用,未使用的 `echarts` 与 `echarts-for-react` 根依赖已移除。 - 当前活动路由无真实图表调用,不新增伪造图表或演示数据。共享 `src/components/XinyuanEChart.jsx` 与 `src/design-system/chartPalette.js` 已建立统一 ECharts 边界:普通图表使用 `--xinyuan-chart-*`,饼图/环形图仅使用独立 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*`,并按稳定分类 key 做确定性冲突消解;页面不直接写颜色或私有图表主题。
- 权威 `theme.css` 逐字节一致;活动页不存在页面级 CSS,私有外观 utility 扫描为零。 - 权威 `theme.css` 逐字节一致;活动页不存在页面级 CSS,私有外观 utility 扫描为零。
## 迁移风险 ## 迁移风险
......
import {
createResolvedXinyuanPieChartColorMap,
getXinyuanEChartsTheme,
} from '@/design-system/chartPalette';
import ReactECharts from 'echarts-for-react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
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 } };
});
}
export default function XinyuanEChart({
ariaLabel,
className = '',
height = 320,
onEvents,
option,
}) {
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 pieKeys = (Array.isArray(option?.series) ? option.series : [option?.series])
.filter((series) => series?.type === 'pie')
.flatMap((series) => (Array.isArray(series.data) ? series.data.map((item) => item?.name) : []));
const pieColorMap = createResolvedXinyuanPieChartColorMap(pieKeys);
const applySeriesTheme = (series) => {
if (series?.type !== 'pie') return series;
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: {
...series.itemStyle,
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(applySeriesTheme)
: applySeriesTheme(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: 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 },
},
};
}, [option]);
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>
);
}
export const XINYUAN_CHART_PALETTE = Object.freeze(
Array.from({ length: 16 }, (_, index) => `var(--rc-chart-${index + 1})`),
);
// Pie and donut slices need more hue separation than linear series. Keep this
// palette independent so bar/line/ranking charts retain their approved colors.
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 createResolvedXinyuanPieChartColorMap(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, resolveXinyuanChartColor(getXinyuanPieChartColor(paletteIndex)));
});
return colorMap;
}
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', '#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(`--xinyuan-pie-chart-${index + 1}`, '#667080')
)),
barRadius: Number.parseFloat(readRootToken('--xinyuan-chart-bar-radius', '8')) || 8,
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: readConcreteColorToken('--xinyuan-chart-tooltip-background', '#ffffff'),
tooltipBorder: readConcreteColorToken('--xinyuan-chart-tooltip-border', '#dedede'),
tooltipShadow: readRootToken('--xinyuan-chart-tooltip-shadow', '0 16px 40px rgb(15 23 42 / 14%)'),
};
}
...@@ -116,6 +116,23 @@ ...@@ -116,6 +116,23 @@
--xinyuan-chart-14: #8A6F63; --xinyuan-chart-14: #8A6F63;
--xinyuan-chart-15: #9A8174; --xinyuan-chart-15: #9A8174;
--xinyuan-chart-16: #5B5964; --xinyuan-chart-16: #5B5964;
/* Pie and donut charts use an independent categorical palette. */
--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;
......
...@@ -219,6 +219,23 @@ ...@@ -219,6 +219,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-only aliases. Never use these for bars, lines or ranking charts. */
--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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment