Commit 5ba517d6 authored by Andy-bubu's avatar Andy-bubu

fix: sync pie chart palette

parent 918096b2
...@@ -49,7 +49,9 @@ Hover/Focus/Selected/Disabled。`src/pages` 下没有页面私有 CSS/Less/SCSS ...@@ -49,7 +49,9 @@ Hover/Focus/Selected/Disabled。`src/pages` 下没有页面私有 CSS/Less/SCSS
- `XinyuanModal`:HeroUI Modal compound 结构、批准宽度和 Surface 层级。 - `XinyuanModal`:HeroUI Modal compound 结构、批准宽度和 Surface 层级。
- `TableActionButton`:HeroUI Button/Dropdown/Tooltip 与统一 Ant Design 图标。 - `TableActionButton`:HeroUI Button/Dropdown/Tooltip 与统一 Ant Design 图标。
- `XinyuanEChart`:`echarts-for-react` 的共享主题边界,统一解析 `--xinyuan-chart-*`,注入 - `XinyuanEChart`:`echarts-for-react` 的共享主题边界,统一解析 `--xinyuan-chart-*`,注入
ARIA、Tooltip、图例、坐标轴和响应式;业务页面只拥有 series/data 等业务配置。 ARIA、Tooltip、图例、坐标轴和响应式;业务页面只拥有 series/data 等业务配置。`type: 'pie'`
的饼图/环形图由同一适配器切换到独立 `--xinyuan-pie-chart-*` 色板,普通柱状图、折线图和排名图继续使用
`--xinyuan-chart-*`,页面不得写入饼图颜色常量或覆盖专用映射。
- `FilterSurface`(共享布局合同):三列以上字段使用 HeroUI `Surface default` 承载,字段使用 - `FilterSurface`(共享布局合同):三列以上字段使用 HeroUI `Surface default` 承载,字段使用
`secondary`,首行最多三个高频字段,剩余字段通过官方 `Disclosure` 展开。操作按字段底部右对齐,顺序 `secondary`,首行最多三个高频字段,剩余字段通过官方 `Disclosure` 展开。操作按字段底部右对齐,顺序
固定为 36px 圆形重置图标、品牌红查询按钮、`sm ghost` 展开/收起触发器;重置通过共享回调同步清空待提交/已提交 固定为 36px 圆形重置图标、品牌红查询按钮、`sm ghost` 展开/收起触发器;重置通过共享回调同步清空待提交/已提交
......
...@@ -41,6 +41,7 @@ ...@@ -41,6 +41,7 @@
## 页面与组件 ## 页面与组件
- [x] 驾驶舱:4 个 Pro KPI;状态饼图和阶段柱图统一使用共享 ECharts 主题适配器 - [x] 驾驶舱:4 个 Pro KPI;状态饼图和阶段柱图统一使用共享 ECharts 主题适配器
- [x] 饼图/环形图使用独立 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*` 色板;普通柱状图、折线图和排名图继续使用 `--xinyuan-chart-*`
- [x] 项目列表/归档/外部填写:Tabs、Surface、Autocomplete、DatePicker、Disclosure - [x] 项目列表/归档/外部填写:Tabs、Surface、Autocomplete、DatePicker、Disclosure
- [x] 全部业务列表:Pro DataGrid、原生选择/排序/固定列、Pagination、ActionBar、EmptyState - [x] 全部业务列表:Pro DataGrid、原生选择/排序/固定列、Pagination、ActionBar、EmptyState
- [x] 项目业务列表 Pagination 同行左侧摘要右侧提供 HeroUI 20/50/100 条页容量切换(无搜索框),页码控制靠右,切换后回到第一页 - [x] 项目业务列表 Pagination 同行左侧摘要右侧提供 HeroUI 20/50/100 条页容量切换(无搜索框),页码控制靠右,切换后回到第一页
......
...@@ -228,6 +228,11 @@ Inter 与系统回退字体之间切换。 ...@@ -228,6 +228,11 @@ Inter 与系统回退字体之间切换。
`#4F6380`、`#61758F`、`#74879A`、`#8A6F63`、`#9A8174`、`#B0A5AD`。单系列优先使用品牌红, `#4F6380`、`#61758F`、`#74879A`、`#8A6F63`、`#9A8174`、`#B0A5AD`。单系列优先使用品牌红,
同一指标的不同状态优先使用品牌红深浅层级;图标始终使用 同一指标的不同状态优先使用品牌红深浅层级;图标始终使用
`AppIcons` 汇出的 Ant Design Icons,ECharts 不作为图标来源。 `AppIcons` 汇出的 Ant Design Icons,ECharts 不作为图标来源。
- 饼图/环形图是普通图表色板之外的独立例外,仅由共享 `XinyuanEChart` 从
`--xinyuan-pie-chart-*` / `--rc-pie-chart-*` 读取颜色。前六个颜色固定为
`#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,其余为批准的低饱和灰蓝、灰紫、暖棕、暖灰和石板灰扩展色;禁止绿色、青绿色、荧光色和彩虹色。
共享适配器先收集完整分类 key,再按稳定 key 确定性分配并处理冲突;筛选、排序、Tab 切换和主题切换均不得改变同一分类的颜色。
该专用色板不影响柱状图、折线图、排名图、KPI 或页面品牌主色,未来新增饼图/环形图必须直接使用共享适配器,页面不得保留旧 `itemStyle.color` 覆盖。
- 所有 Autocomplete、引用、用户和材料选择器的下拉选项只展示一行业务标题;不得在第二行 - 所有 Autocomplete、引用、用户和材料选择器的下拉选项只展示一行业务标题;不得在第二行
展示内部 ID、用户名、文件名、年份或其它技术描述。描述字段可以继续保留用于搜索、匹配和 展示内部 ID、用户名、文件名、年份或其它技术描述。描述字段可以继续保留用于搜索、匹配和
数据处理,但不能渲染为选项副标题。应用切换卡片的业务描述不属于选择器选项,可按应用切换规范保留。 数据处理,但不能渲染为选项副标题。应用切换卡片的业务描述不属于选择器选项,可按应用切换规范保留。
......
...@@ -288,6 +288,9 @@ HeroUI Context。`@heroui/styles@3.2.4` 的两个 pnpm peer 虚拟目录仅分 ...@@ -288,6 +288,9 @@ HeroUI Context。`@heroui/styles@3.2.4` 的两个 pnpm peer 虚拟目录仅分
按品牌红→暖色→中性→灰蓝的协调顺序取色,不使用绿色、荧光色或彩虹式跳色; 按品牌红→暖色→中性→灰蓝的协调顺序取色,不使用绿色、荧光色或彩虹式跳色;
通过稳定业务顺序/业务 key 分配相邻色阶,禁止页面写入随机或局部色值;项目状态分布按 通过稳定业务顺序/业务 key 分配相邻色阶,禁止页面写入随机或局部色值;项目状态分布按
计划、执行、归档、整改、待启动、完成的顺序连续取用色板,筛选后 key 颜色保持不变。 计划、执行、归档、整改、待启动、完成的顺序连续取用色板,筛选后 key 颜色保持不变。
- 饼图/环形图已独立使用 `--xinyuan-pie-chart-*` / `--rc-pie-chart-*` 专用色板,前六个颜色为
`#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`;共享适配器按稳定分类 key
确定性分配并处理冲突,过滤、排序、Tab 和主题变化不改色,普通 Bar/Line/Ranking 色板未改变。
- 状态环图已启用 ECharts 外部分类标签和 `labelLine` 引导线,标签显示真实状态与数量, - 状态环图已启用 ECharts 外部分类标签和 `labelLine` 引导线,标签显示真实状态与数量,
使用 `labelLayout` 根据 `labelLinePoints` 将标签锚定到各自折线终点,避免文案与折线错位。 使用 `labelLayout` 根据 `labelLinePoints` 将标签锚定到各自折线终点,避免文案与折线错位。
- 状态环图扇区统一使用 `padAngle: 1`、2px Surface 色间隙和 `borderRadius: 10`,扇区边界紧凑清晰且不改变真实数量比例;图表模块外层使用 24px 圆角。 - 状态环图扇区统一使用 `padAngle: 1`、2px Surface 色间隙和 `borderRadius: 10`,扇区边界紧凑清晰且不改变真实数量比例;图表模块外层使用 24px 圆角。
......
import { import {
createResolvedXinyuanPieChartColorMap,
getXinyuanEChartsTheme, getXinyuanEChartsTheme,
} from '@/design-system/chartPalette'; } from '@/design-system/chartPalette';
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
...@@ -62,15 +63,51 @@ export default function XinyuanEChart({ ...@@ -62,15 +63,51 @@ export default function XinyuanEChart({
const applyPieTheme = (series) => { const applyPieTheme = (series) => {
if (series?.type !== 'pie') return series; if (series?.type !== 'pie') return series;
const pieColorMap = createResolvedXinyuanPieChartColorMap(
Array.isArray(series.data)
? series.data.map((item) => item?.name)
: [],
);
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();
const { color: _itemColor, ...itemStyle } = item.itemStyle || {};
const itemLabelLine = item.labelLine;
const { color: _lineColor, ...labelLineStyle } =
itemLabelLine?.lineStyle || {};
return {
...item,
itemStyle: {
...itemStyle,
color: pieColorMap.get(key)
|| theme.piePalette[index % theme.piePalette.length],
},
...(itemLabelLine ? {
labelLine: {
...itemLabelLine,
...(itemLabelLine.lineStyle ? { lineStyle: labelLineStyle } : {}),
},
} : {}),
};
})
: series.data;
const { color: _seriesColor, ...seriesItemStyle } = series.itemStyle || {};
return { return {
...series, ...series,
color: theme.piePalette,
itemStyle: { itemStyle: {
...seriesItemStyle,
borderColor: theme.sectorGap, borderColor: theme.sectorGap,
borderRadius: theme.sectorRadius, borderRadius: theme.sectorRadius,
borderWidth: 2, borderWidth: 2,
...series.itemStyle,
}, },
padAngle: series.padAngle ?? 1, data: pieData,
padAngle: 1,
}; };
}; };
const themedSeries = Array.isArray(option?.series) const themedSeries = Array.isArray(option?.series)
......
...@@ -17,6 +17,12 @@ export const XINYUAN_CHART_PALETTE = Object.freeze([ ...@@ -17,6 +17,12 @@ export const XINYUAN_CHART_PALETTE = Object.freeze([
'var(--rc-chart-16)', 'var(--rc-chart-16)',
]); ]);
// Pie and donut slices use an independent palette so their adjacent sectors
// remain distinguishable without changing the approved bar/line 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({ export const XINYUAN_CHART_SEMANTIC = Object.freeze({
primary: 'var(--rc-chart-primary)', primary: 'var(--rc-chart-primary)',
risk: 'var(--rc-chart-risk)', risk: 'var(--rc-chart-risk)',
...@@ -106,6 +112,37 @@ function readRootToken(tokenName, fallback = '') { ...@@ -106,6 +112,37 @@ function readRootToken(tokenName, fallback = '') {
.trim() || fallback; .trim() || fallback;
} }
const concreteColorCache = new Map();
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 = window.getComputedStyle(probe).color || fallback;
probe.remove();
concreteColorCache.set(tokenName, { raw, value: resolved });
return resolved;
}
export function getXinyuanPieChartColor(index = 0) {
return XINYUAN_PIE_CHART_PALETTE[
Math.max(0, Number(index) || 0) % XINYUAN_PIE_CHART_PALETTE.length
];
}
export function resolveXinyuanChartColor(color, fallback = '') { export function resolveXinyuanChartColor(color, fallback = '') {
const match = String(color || '').trim().match(/^var\((--[^)]+)\)$/); const match = String(color || '').trim().match(/^var\((--[^)]+)\)$/);
return match ? readRootToken(match[1], fallback) : String(color || fallback); return match ? readRootToken(match[1], fallback) : String(color || fallback);
...@@ -120,6 +157,37 @@ export function createResolvedXinyuanChartColorMap(keys = [], options = {}) { ...@@ -120,6 +157,37 @@ export function createResolvedXinyuanChartColorMap(keys = [], options = {}) {
); );
} }
export function createResolvedXinyuanPieChartColorMap(keys = []) {
const stableKeys = [...new Set(keys.map(normalizeChartKey).filter(Boolean))]
.sort((left, right) => left.localeCompare(right, 'zh-CN'));
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,
readConcreteColorToken(
`--rc-pie-chart-${paletteIndex + 1}`,
getXinyuanPieChartColor(paletteIndex),
),
);
});
return colorMap;
}
export function getXinyuanEChartsTheme() { export function getXinyuanEChartsTheme() {
const documentFont = typeof document === 'undefined' const documentFont = typeof document === 'undefined'
? 'Inter, sans-serif' ? 'Inter, sans-serif'
...@@ -134,9 +202,12 @@ export function getXinyuanEChartsTheme() { ...@@ -134,9 +202,12 @@ export function getXinyuanEChartsTheme() {
palette: Array.from({ length: 16 }, (_, index) => ( palette: Array.from({ length: 16 }, (_, index) => (
readRootToken(`--xinyuan-chart-${index + 1}`, '#667085') readRootToken(`--xinyuan-chart-${index + 1}`, '#667085')
)), )),
piePalette: Array.from({ length: 16 }, (_, index) => (
readConcreteColorToken(`--xinyuan-pie-chart-${index + 1}`, '#667080')
)),
tooltipBackground: readRootToken('--xinyuan-chart-tooltip-background', '#ffffff'), tooltipBackground: readRootToken('--xinyuan-chart-tooltip-background', '#ffffff'),
tooltipBorder: readRootToken('--xinyuan-chart-tooltip-border', '#dedede'), tooltipBorder: readRootToken('--xinyuan-chart-tooltip-border', '#dedede'),
sectorGap: readRootToken('--xinyuan-chart-sector-gap', '#ffffff'), sectorGap: readConcreteColorToken('--xinyuan-chart-sector-gap', '#ffffff'),
sectorRadius: Number.parseFloat( sectorRadius: Number.parseFloat(
readRootToken('--xinyuan-chart-sector-radius', '10'), readRootToken('--xinyuan-chart-sector-radius', '10'),
) || 10, ) || 10,
......
...@@ -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: independent muted palette with higher hue separation. */
--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;
......
...@@ -164,6 +164,23 @@ ...@@ -164,6 +164,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 series are intentionally separate from bar/line 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);
......
...@@ -3,7 +3,6 @@ import { EmptyStateSurface } from '@/components/DataTable'; ...@@ -3,7 +3,6 @@ import { EmptyStateSurface } from '@/components/DataTable';
import XinyuanModal from '@/components/XinyuanModal'; import XinyuanModal from '@/components/XinyuanModal';
import XinyuanEChart from '@/components/XinyuanEChart'; import XinyuanEChart from '@/components/XinyuanEChart';
import { import {
createResolvedXinyuanChartColorMap,
getXinyuanEChartsTheme, getXinyuanEChartsTheme,
} from '@/design-system/chartPalette'; } from '@/design-system/chartPalette';
import { import {
...@@ -128,10 +127,6 @@ function ProjectPanel({ title, records, emptyText, onOpen }) { ...@@ -128,10 +127,6 @@ function ProjectPanel({ title, records, emptyText, onOpen }) {
function StatusChart({ data }) { function StatusChart({ data }) {
const option = useMemo(() => { const option = useMemo(() => {
const theme = getXinyuanEChartsTheme(); const theme = getXinyuanEChartsTheme();
const colorMap = createResolvedXinyuanChartColorMap(
data.map((item) => item.name),
{ order: STATUS_CHART_ORDER },
);
return { return {
legend: { legend: {
...@@ -142,16 +137,7 @@ function StatusChart({ data }) { ...@@ -142,16 +137,7 @@ function StatusChart({ data }) {
avoidLabelOverlap: true, avoidLabelOverlap: true,
data: [...data].sort( data: [...data].sort(
(left, right) => getStatusChartOrder(left.name) - getStatusChartOrder(right.name), (left, right) => getStatusChartOrder(left.name) - getStatusChartOrder(right.name),
).map((item) => ({ ),
...item,
itemStyle: { color: colorMap.get(item.name) },
labelLine: {
lineStyle: {
color: colorMap.get(item.name),
width: 1.5,
},
},
})),
emphasis: { emphasis: {
scaleSize: 4, scaleSize: 4,
}, },
......
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