Commit 2482eb15 authored by Andy-bubu's avatar Andy-bubu

feat: add dedicated pie chart palette

parent 6db5196a
......@@ -30,6 +30,7 @@
- 弹窗头部说明由 HeroUI `Description` 负责;字段标签由共享 `getFieldDisplayLabel` 从字段定义和中文别名表解析,`metadata.*` 只存在于数据层。文本、选择、日期、人员/引用和附件的提示均由对应 HeroUI 控件在内部展示,禁止再在控件下方复制同一句自动提示。
- 4xl 动态表单由共享 `DynamicRecordDialog` 负责等宽双列编排,字段类型决定是否跨列;独立流程页没有 AppShell 时保留 `RecordWorkbench` 内容标题,主应用列表通过 `showContentTitle={false}` 只使用 Navbar 标题。
- KPI 只能使用 HeroUI Pro `KPI` compound API;共享层不得以 Card class 重新绘制 KPI 的背景、描边、阴影或圆角。
- 图表颜色由共享 Token 和适配器负责:Bar/Line/Ranking 继续使用 `--rc-chart-*`,Pie/Donut 仅使用 `--rc-pie-chart-*`。`applyXinyuanPieChartPalette` 只改 Pie 系列的颜色并按完整稳定分类 key 确定性映射,保留 radius、center、label、labelLine、legend、tooltip、事件及布局;没有真实饼图业务时不得新增伪造页面或数据。
- 所有可截断业务文本由共享 `DataTableText`、`DataTableTextPair` 或 `DataTableSecondaryText` 提供,并以 HeroUI `Tooltip` 暴露完整值;页面不得再写独立截断、换行或原生 `title` 规则。
- 开发工具产物不属于页面依赖图:Umi webpack watcher 在共享配置中忽略 `.playwright-cli`、`docs`、`dist` 和 `artifacts`,保证 HeroUI/Inter 样式不会因验收日志写入而反复 HMR。
......
......@@ -42,4 +42,4 @@
- [x] 对齐问题整改 `6763c26`:详情加载完整 View,Detail 字段权限整套优先、空配置整套回退 Grid、`disableColumns` 不可旁路且切换 Detail 立即重算。
- [x] Playwright 权限矩阵覆盖其他应用管理、顶层管理、系统账号、本应用管理、管理员 `disableColumns`、Detail 优先、Grid 正反向回退、不逐字段合并和多 Detail 切换;控制台为零。
剩余说明:业务没有活动图表路由,因此没有凭空添加 KPI/图表数据;`recharts` 仅保留原依赖且无源码 import。应用切换遵循 `model_analysis` 专项基准和鑫元规范,使用动态分组、移动一列及 `sm` 起固定两列。当前真实 Detail View 没有 editable file 字段,未为验收修改后端 View 配置;文件事务由浏览器 fixture 验收。
剩余说明:业务没有活动图表路由,因此没有凭空添加 KPI/图表数据;`recharts` 仅保留原依赖且无源码 import。饼图/环形图已预置独立 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*` 色板及 `applyXinyuanPieChartPalette` 稳定映射,前六色为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,未改变普通图表 Token。应用切换遵循 `model_analysis` 专项基准和鑫元规范,使用动态分组、移动一列及 `sm` 起固定两列。当前真实 Detail View 没有 editable file 字段,未为验收修改后端 View 配置;文件事务由浏览器 fixture 验收。
......@@ -21,6 +21,8 @@
- KPI 使用 HeroUI Pro KPI;图表使用 HeroUI Pro 图表与 ChartTooltip。
- 鑫元唯一 Primary Brand Color 为 `#DA241A`;HeroUI `--accent`、`--primary`、focus、hover、pressed 和按钮 disabled 状态均从品牌红阶派生。
- `--xinyuan-chart-1` 至 `--xinyuan-chart-16` 是独立于 UI 状态色的可视化 Token,采用品牌红、暖中性色、灰紫和灰蓝的低饱和顺序,不包含绿色或荧光色。单系列使用 `--rc-chart-primary`,多系列通过 `src/utils/chartColors.js` 按稳定业务 key 分配颜色。
- 饼图 / 环形图是普通分类色板之外的专项例外,使用 `--xinyuan-pie-chart-1` 至 `--xinyuan-pie-chart-16`(别名 `--rc-pie-chart-*`)。前六个固定为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,后续只使用批准的低饱和灰蓝、灰紫、暖棕和暖灰。禁止绿色、青绿色、荧光色和彩虹式配色;普通柱状图、折线图、排名图和品牌主色继续使用原 `--xinyuan-chart-*`。
- 未来 Pie/Donut 必须通过 `src/utils/chartColors.js` 的 `applyXinyuanPieChartPalette` 或等价共享适配器接入:完整分类 key 经稳定排序、哈希和冲突消解后分配颜色,筛选、排序、分页、Tab 和主题切换不得改变分类颜色;页面传入的旧 `itemStyle.color` 不得覆盖专项色板。
- Modal/Dropdown/Popover/Tooltip 等 Portal 必须继承根级深色主题。
- Modal 面板保持 HeroUI Overlay 的 24px 圆角;弹窗内结构性 Surface 只声明官方 `secondary/tertiary` variant 和布局间距,不再通过业务 JSX 覆盖圆角、背景、描边或阴影。详情中的关联记录属于 L2 详情 Surface 内的静态 L3 内容,使用官方 `Surface variant="tertiary"` 承载,并在其内部使用共享 `DataTable` 的 HeroUI Pro DataGrid `secondary` variant;表头保持 `surface-secondary`、表体保持透明并继承 L3 Surface。禁止页面自定义颜色、描边、阴影或 HeroUI 内部 BEM/slot 外观。
- Modal 统一带有 `rc-dialog-panel rc-dialog-panel--{size}` 宽度阶梯;宽度只由共享布局类负责,背景、圆角、阴影、关闭和状态由 HeroUI Modal compound API 提供。
......
......@@ -37,6 +37,7 @@
- Portal:Headless UI Dialog、Combobox、Listbox。
- 列表:自制 Table、选择列、分页、空状态和工具栏。
- 图表:未发现活动业务图表、ECharts 私有主题或自制 Tooltip;源码没有 `PieChart`/`BarChart`/`ComposedChart`/`ChartTooltip` 活动页面。`recharts` 仍是原始依赖并保留在锁文件中,但没有活动 import,不制造测试数据。
- 饼图 / 环形图专项审计:当前仍无真实 Pie/Donut 调用点,未新增伪造图表数据或页面。已新增 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*` 专用 Token、稳定分类 key 冲突消解映射和 `applyXinyuanPieChartPalette` 共享适配器;未来接入时不得由页面直接传色或改动普通图表色板。
- 原生控件:文件上传、日期/日期时间输入。
- 私有视觉:大量 zinc/blue/red class、边框、背景、圆角、阴影和状态样式。
......
......@@ -107,6 +107,23 @@
--xinyuan-chart-14: #8A6F63;
--xinyuan-chart-15: #9A8174;
--xinyuan-chart-16: #B0A5AD;
/* Pie / donut only. This palette is independent from bar and line colors. */
--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: #B0A5AD;
--xinyuan-pie-chart-14: #AFA5B1;
--xinyuan-pie-chart-15: #897A73;
--xinyuan-pie-chart-16: #556B88;
--xinyuan-chart-sector-gap: var(--surface);
--xinyuan-chart-sector-radius: 10px;
--xinyuan-chart-grid: #e8e8e8;
......
......@@ -169,6 +169,23 @@
--rc-chart-14: var(--xinyuan-chart-14);
--rc-chart-15: var(--xinyuan-chart-15);
--rc-chart-16: var(--xinyuan-chart-16);
/* Pie / donut series use a dedicated categorical palette. */
--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);
/* Stable semantic meanings. The palette is intentionally free of green and neon hues. */
--rc-chart-primary: var(--rc-chart-1);
--rc-chart-risk: var(--rc-chart-1);
......
......@@ -3,6 +3,16 @@ export const XINYUAN_CHART_TOKENS = Object.freeze(
Array.from({ length: 16 }, (_, index) => `--rc-chart-${index + 1}`),
);
// Pie and donut sectors use a separate, restrained palette. Keep this list
// independent so adding a pie chart cannot alter bar/line series colors.
export const XINYUAN_PIE_CHART_TOKENS = Object.freeze(
Array.from({ length: 16 }, (_, index) => `--rc-pie-chart-${index + 1}`),
);
export const XINYUAN_PIE_CHART_PALETTE = Object.freeze(
XINYUAN_PIE_CHART_TOKENS.map((token) => `var(${token})`),
);
export function getChartColorToken(index = 0) {
const normalizedIndex = Math.max(0, Number(index) || 0);
if (normalizedIndex >= XINYUAN_CHART_TOKENS.length) {
......@@ -11,6 +21,14 @@ export function getChartColorToken(index = 0) {
return `var(${XINYUAN_CHART_TOKENS[normalizedIndex]})`;
}
export function getXinyuanPieChartColor(index = 0) {
const normalizedIndex = Math.max(0, Number(index) || 0);
if (normalizedIndex >= XINYUAN_PIE_CHART_TOKENS.length) {
throw new RangeError('Xinyuan pie charts support at most 16 distinct categories.');
}
return XINYUAN_PIE_CHART_PALETTE[normalizedIndex];
}
function hashChartKey(key) {
let hash = 2166136261;
for (let index = 0; index < key.length; index += 1) {
......@@ -41,3 +59,110 @@ export function createStableChartColorMap(keys = []) {
}),
);
}
function normalizePieChartKey(key) {
return String(key ?? '').trim();
}
function createStablePieChartColorMap(keys = []) {
const uniqueKeys = [...new Set(keys.map(normalizePieChartKey).filter(Boolean))]
.sort((left, right) => left.localeCompare(right, 'zh-Hans'));
if (uniqueKeys.length > XINYUAN_PIE_CHART_TOKENS.length) {
throw new RangeError('Xinyuan pie charts support at most 16 distinct categories.');
}
// Stable hashing plus linear probing keeps a category's color unchanged
// after filtering, sorting, pagination, or tab changes.
const usedIndexes = new Set();
return new Map(uniqueKeys.map((key) => {
let paletteIndex = hashChartKey(key) % XINYUAN_PIE_CHART_TOKENS.length;
while (usedIndexes.has(paletteIndex)) {
paletteIndex = (paletteIndex + 1) % XINYUAN_PIE_CHART_TOKENS.length;
}
usedIndexes.add(paletteIndex);
return [key, getXinyuanPieChartColor(paletteIndex)];
}));
}
export { createStablePieChartColorMap };
const resolvedPieTokenCache = new Map();
function resolvePieChartToken(token) {
if (typeof document === 'undefined' || typeof window === 'undefined') return token;
const match = String(token).match(/^var\((--[^)]+)\)$/);
if (!match) return token;
const raw = window.getComputedStyle(document.documentElement)
.getPropertyValue(match[1]).trim();
const cached = resolvedPieTokenCache.get(match[1]);
if (cached?.raw === raw) return cached.value;
if (!raw) return token;
// A detached color probe resolves nested custom-property aliases and keeps
// ECharts options concrete, as required by the chart color contract.
const probe = document.createElement('span');
probe.style.color = raw;
if (!probe.style.color) return token;
probe.style.position = 'absolute';
probe.style.pointerEvents = 'none';
document.body?.appendChild(probe);
const value = window.getComputedStyle(probe).color || token;
probe.remove();
resolvedPieTokenCache.set(match[1], { raw, value });
return value;
}
export function createResolvedXinyuanPieChartColorMap(keys = []) {
return new Map(
Array.from(createStablePieChartColorMap(keys)).map(([key, token]) => [
key,
resolvePieChartToken(token),
]),
);
}
/**
* Apply only the dedicated pie/donut colors to an ECharts option. All other
* series and all non-color pie settings are returned unchanged.
*/
export function applyXinyuanPieChartPalette(option) {
if (!option || (!Array.isArray(option.series) && !option.series)) return option;
const seriesList = Array.isArray(option.series) ? option.series : [option.series];
const pieSeries = seriesList.filter((series) => series?.type === 'pie');
if (pieSeries.length === 0) return option;
const categoryKeys = pieSeries.flatMap((series) => (
Array.isArray(series.data) ? series.data.map((item) => (
item && typeof item === 'object' ? item.name : item
)) : []
));
const colorMap = createResolvedXinyuanPieChartColorMap(categoryKeys);
const piePalette = XINYUAN_PIE_CHART_PALETTE.map(resolvePieChartToken);
const themedSeries = seriesList.map((series) => {
if (series?.type !== 'pie') return series;
const seriesItemStyle = { ...(series.itemStyle || {}) };
delete seriesItemStyle.color;
return {
...series,
color: piePalette,
data: Array.isArray(series.data) ? series.data.map((item, index) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) return item;
const key = normalizePieChartKey(item.name);
return {
...item,
itemStyle: {
...item.itemStyle,
color: colorMap.get(key) || piePalette[index % piePalette.length],
},
};
}) : series.data,
itemStyle: seriesItemStyle,
};
});
return {
...option,
series: Array.isArray(option.series) ? themedSeries : themedSeries[0],
};
}
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