Commit 9ad0e27c authored by Andy-bubu's avatar Andy-bubu

feat: add dedicated pie chart color palette

parent 5b7bbdec
......@@ -42,6 +42,7 @@
- Tabs:直接使用 HeroUI Tabs compound API。页面级、模块级和详情级都按文字内容确定各项宽度,只在 compound slot 的 `className` 上设置 `Tabs/ListContainer: w-fit max-w-full min-w-0`、`List: w-max min-w-max`、`Tab: w-auto min-w-max flex-none whitespace-nowrap`。标签始终单行完整展示,项目不得均分或固定 Tab 宽度,溢出只由 `Tabs.ListContainer` 的官方 `ScrollShadow` 处理。选中态、尺寸、圆角、颜色、阴影和动效采用官方默认,仅由 `theme.css` 提供主题值;禁止全局选择 `.tabs__*` 重画外观。
- 字段来源切换(例如“材料中心 / 本地上传”)使用 HeroUI `primary` Tabs;Tabs 根、Panel 和 Panel 内容容器为 `w-full min-w-0`,Panel 仅使用 `mt-3 p-0` 调整布局间距,列表仍保持文字自适应宽度,Panel 内选择框和上传控件统一 `fullWidth`,确保控件与弹窗字段列通栏对齐。
- 图表:HeroUI Pro PieChart、BarChart、ComposedChart、KPI 和 ChartTooltip;共享 `chartPalette.js` 导出 `tokens.css` 中的 8 个分类色、语义图表色引用及同图唯一分配器。分类图表必须一次传入完整系列 key,在 8 个系列内确定性消除哈希撞色;页面不得直接用 `success/danger/warning/foreground` 组装色板。图表 Card 主标题在 HeroUI `Card.Header` 内统一左对齐并与正文左边缘一致;分类层级、范围和统计口径参数以紧凑 `secondary` Autocomplete 位于 Header 右侧同一行,真正的并列视图切换才使用 HeroUI Tabs。禁止按图表类型切换为居中标题。图例文字通过公开 label slot 使用 `--rc-chart-legend-font-size` / `--rc-chart-legend-line-height`,不得覆盖第三方图表内部类。交叉维度数据(如问题来源 × 检查单位)优先使用带单元格数量的 ECharts Heatmap,非零单元使用统一语义主色、零值保持当前 Surface,避免多系列柱形在窄卡片内重叠;保留原统计数据与业务回调,不保留 ECharts 私有主题。
- 饼图 / 环形图是普通分类色板之外的专用例外:共享适配器使用 `--rc-pie-chart-*`,前六个颜色依次为品牌红、灰蓝、灰紫、暖金、暖棕、石板灰,仅用于扇区;柱状图、折线图、数据卡片和页面主色继续使用原 `--rc-chart-*` Token。按稳定分类 key 确定性取色,扇区数量不足时不填充多余颜色,超过六类从批准的低饱和扩展色继续取用,禁止绿色或彩虹色。
- 图表 Tooltip:共享 `colorizeXinyuanChartTooltipPayload` 在进入 HeroUI Pro `ChartTooltipContent` 前把 Tooltip indicator 颜色归一化为当前图形 mark/图例的同一 Token,并排除 `none`/`transparent`。环图、单系列柱图和多系列组合图均显示官方 8px 圆点;页面不复制 Tooltip DOM 或样式。
- 图表焦点:HeroUI Pro 图表根统一使用 `rc-chart-interaction`。共享层通过 `:focus:not(:focus-visible)` 隐藏鼠标点击产生的浏览器原生蓝框,通过 `:focus-visible` 和主题 `--focus` 保留键盘焦点;不得禁用 Recharts `accessibilityLayer`,也不得修改图表点击回调。
- KPI:页面直接组合 HeroUI Pro `KPI` compound API,Card 根使用 `rc-card-surface rc-dashboard-card`,`KPI.Icon` 使用共享 `rc-metric-icon-surface`。共享九色 Token 解决 Pro 原生 `status` 只有三种时的无背景回退,浅底统一为 9% tone(neutral 8%)。`.rc-kpi-grid data-count` 负责 1–9 项栅格和无显式 `data-tone` 时的顺序唯一配色;业务可覆盖语义 tone,但同组不得重复图标或颜色。超过九项拆分模块,页面不得自定义色值、尺寸和背景。
......
......@@ -163,6 +163,8 @@ Surface 用于表达页面内的包含关系,Overlay 用于表达浮动关系
`#DA241A`、`#B83A32`、`#C95A52`、`#D47A70`、`#A65A45`、`#B87958`、`#C99772`、`#8A6F63`、`#9A8174`、`#6F6674`、`#807783`、`#938B96`、`#4F6380`、`#61758F`、`#74879A`、`#B0A4A0`
- 饼图 / 环形图使用独立的 `--xinyuan-pie-chart-1` 至 `--xinyuan-pie-chart-16`(别名 `--rc-pie-chart-*`)色板,不改动普通柱状图、折线图、排名图或 UI 主色。前六个 Token 固定为 `#DA241A`、`#3F5F86`、`#8A668F`、`#C28A3D`、`#8A6253`、`#667080`,按稳定分类 key 确定性取用并用冲突消解保证同图不重复;超过六类继续使用同组低饱和灰蓝、灰紫、暖棕和暖灰扩展色,禁止引入绿色、青绿色、荧光色和彩虹色。过滤、排序和主题切换不能改变同一分类的身份颜色。
- 单系列统一使用 `--rc-chart-primary`(品牌红);同一指标的状态使用同色相深浅;多系列按稳定业务 key 确定性分配协调色,不按实时排序改色。语义映射为:风险 `--rc-chart-risk`、提醒 `--rc-chart-warning`、正向结果 `--rc-chart-positive`(灰蓝)、对比 `--rc-chart-comparison`、中性 `--rc-chart-neutral`。
- 数字卡片使用 `@heroui-pro/react/kpi`。
......
import {
createResolvedXinyuanPieChartColorMap,
getXinyuanEChartsTheme,
} from '@/design-system/chartPalette';
import ReactECharts from 'echarts-for-react';
......@@ -95,15 +96,39 @@ export default function XinyuanEChart({
const applyPieTheme = (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();
return {
...item,
itemStyle: {
...item.itemStyle,
// Pie/donut colors always come from the dedicated palette. This
// keeps adjacent sectors distinguishable and prevents legacy
// page-level colors from leaking into the specialized treatment.
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(series.data, {
data: forceItemStyle(pieData, {
borderColor: theme.sectorGap,
borderRadius: theme.sectorRadius,
borderWidth: theme.sectorGapWidth,
......
......@@ -17,6 +17,12 @@ export const XINYUAN_CHART_PALETTE = Object.freeze([
'var(--rc-chart-16)',
]);
// 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)',
......@@ -46,6 +52,12 @@ export function getXinyuanChartColor(key, fallbackIndex = 0) {
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();
......@@ -154,6 +166,30 @@ export function createResolvedXinyuanChartColorMap(keys = []) {
);
}
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, keyIndex) => {
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'
......@@ -169,6 +205,9 @@ export function getXinyuanEChartsTheme() {
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,
......
......@@ -93,6 +93,23 @@
--xinyuan-chart-14: #61758F;
--xinyuan-chart-15: #74879A;
--xinyuan-chart-16: #B0A4A0;
/* Pie / donut only: higher-hue-separation palette. Keep 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-bar-radius: 8px;
--xinyuan-chart-sector-gap: var(--surface);
--xinyuan-chart-sector-gap-width: 2px;
......
......@@ -165,6 +165,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 are intentionally separate from bar/line categorical 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-risk: var(--rc-chart-2);
--rc-chart-warning: var(--rc-chart-6);
......
import { DataTable } from '@/components/DataTable';
import {
createResolvedXinyuanChartColorMap,
getXinyuanChartColor,
createResolvedXinyuanPieChartColorMap,
getXinyuanPieChartColor,
getXinyuanEChartsTheme,
resolveXinyuanChartColor,
XINYUAN_CHART_SEMANTIC,
......@@ -164,7 +164,7 @@ function FocusIssueDetailDialog({ issueType, items = [], onClose }) {
);
}
function DonutChart({ data, centerLabel, colorForItem }) {
function DonutChart({ data, centerLabel }) {
const total = data.reduce((sum, item) => sum + Number(item.value || 0), 0);
const [viewportWidth, setViewportWidth] = useState(
() => (typeof window === 'undefined' ? 1440 : window.innerWidth),
......@@ -178,7 +178,7 @@ function DonutChart({ data, centerLabel, colorForItem }) {
const option = useMemo(() => {
const theme = getXinyuanEChartsTheme();
const colorMap = createResolvedXinyuanChartColorMap(data.map((item) => item.name));
const colorMap = createResolvedXinyuanPieChartColorMap(data.map((item) => item.name));
const legendData = data.map((item) => String(item.name ?? '').trim()).filter(Boolean);
const estimatedLegendWidth = legendData.reduce(
(width, label) => width + (Array.from(label).length * 13) + 28,
......@@ -220,11 +220,8 @@ function DonutChart({ data, centerLabel, colorForItem }) {
data: data.map((item, index) => ({
...item,
itemStyle: {
color: resolveXinyuanChartColor(
colorForItem?.(item, index)
|| colorMap.get(String(item.name ?? '').trim())
|| getXinyuanChartColor(item.name, index),
),
color: colorMap.get(String(item.name ?? '').trim())
|| resolveXinyuanChartColor(getXinyuanPieChartColor(index)),
},
})),
label: {
......@@ -267,7 +264,7 @@ function DonutChart({ data, centerLabel, colorForItem }) {
}],
tooltip: { trigger: 'item', valueFormatter: (value) => `${formatCount(value)} 条` },
};
}, [centerLabel, colorForItem, data, total, viewportWidth]);
}, [centerLabel, data, total, viewportWidth]);
return <XinyuanEChart ariaLabel={`${centerLabel}分布图`} option={option} />;
}
......@@ -759,13 +756,6 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<DonutChart
data={summary.qualityRatingDistribution}
centerLabel="评级记录"
colorForItem={(item) => (
item.name === '高'
? XINYUAN_CHART_SEMANTIC.positive
: item.name === '中'
? XINYUAN_CHART_SEMANTIC.warning
: XINYUAN_CHART_SEMANTIC.risk
)}
/>
) : (
<EmptyBlock title="暂无综合整改质效结果数据" />
......
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