Optimize the Color Replacement Scheme for Pie Charts

This commit is contained in:
马一丁
2025-11-25 15:42:00 +08:00
parent a7ff9edd29
commit 2e0a526d22
2 changed files with 104 additions and 15 deletions

View File

@@ -278,6 +278,25 @@ class ChartToSVGConverter:
# 其他格式(十六进制、颜色名等)直接返回
return color
def _ensure_visible_color(self, color: Any, fallback: str, min_alpha: float = 0.6) -> Any:
"""
确保颜色在渲染时可见:避免透明值并提升过低的不透明度
"""
base_color = fallback if color in (None, "", "transparent") else color
parsed = self._parse_color(base_color)
fallback_parsed = self._parse_color(fallback)
if isinstance(parsed, tuple):
if len(parsed) == 4:
r, g, b, a = parsed
return (r, g, b, max(a, min_alpha))
return parsed
if isinstance(parsed, str) and parsed.lower() == "transparent":
return fallback_parsed
return parsed if parsed is not None else fallback_parsed
def _get_colors(self, datasets: List[Dict[str, Any]]) -> List[str]:
"""
获取图表颜色
@@ -659,12 +678,17 @@ class ChartToSVGConverter:
fig, ax = self._create_figure(width, height, dpi, title)
# 获取颜色
colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])
if not isinstance(colors, list):
colors = self.DEFAULT_COLORS[:len(labels)]
raw_colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])
if not isinstance(raw_colors, list):
raw_colors = self.DEFAULT_COLORS[:len(labels)]
# 【修复】解析每个颜色将CSS格式转换为matplotlib格式
colors = [self._parse_color(c) for c in colors]
colors = [
self._ensure_visible_color(
raw_colors[i] if i < len(raw_colors) else None,
self.DEFAULT_COLORS[i % len(self.DEFAULT_COLORS)]
)
for i in range(len(labels))
]
# 绘制饼图
wedges, texts, autotexts = ax.pie(
@@ -713,12 +737,17 @@ class ChartToSVGConverter:
fig, ax = self._create_figure(width, height, dpi, title)
# 获取颜色
colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])
if not isinstance(colors, list):
colors = self.DEFAULT_COLORS[:len(labels)]
raw_colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])
if not isinstance(raw_colors, list):
raw_colors = self.DEFAULT_COLORS[:len(labels)]
# 【修复】解析每个颜色将CSS格式转换为matplotlib格式
colors = [self._parse_color(c) for c in colors]
colors = [
self._ensure_visible_color(
raw_colors[i] if i < len(raw_colors) else None,
self.DEFAULT_COLORS[i % len(self.DEFAULT_COLORS)]
)
for i in range(len(labels))
]
# 绘制圆环图通过设置wedgeprops实现中空效果
wedges, texts, autotexts = ax.pie(
@@ -889,9 +918,17 @@ class ChartToSVGConverter:
ax.set_title(title, fontsize=14, fontweight='bold', pad=20)
# 获取颜色
colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])
if not isinstance(colors, list):
colors = self.DEFAULT_COLORS[:len(labels)]
raw_colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])
if not isinstance(raw_colors, list):
raw_colors = self.DEFAULT_COLORS[:len(labels)]
colors = [
self._ensure_visible_color(
raw_colors[i] if i < len(raw_colors) else None,
self.DEFAULT_COLORS[i % len(self.DEFAULT_COLORS)]
)
for i in range(len(labels))
]
# 计算角度
theta = np.linspace(0, 2 * np.pi, len(labels), endpoint=False)

View File

@@ -2965,6 +2965,41 @@ function parseRgbString(color) {
return [parts[0], parts[1], parts[2]].map(v => Math.max(0, Math.min(255, v)));
}
function alphaFromColor(color) {
if (typeof color !== 'string') return null;
const raw = color.trim();
if (!raw) return null;
if (raw.toLowerCase() === 'transparent') return 0;
const extractAlpha = (source) => {
const match = source.match(/rgba?\s*\(([^)]+)\)/i);
if (!match) return null;
const parts = match[1].split(',').map(p => p.trim());
if (source.toLowerCase().startsWith('rgba') && parts.length >= 2) {
const alphaToken = parts[parts.length - 1];
const isPercent = /%$/.test(alphaToken);
const alphaVal = parseFloat(alphaToken.replace('%', ''));
if (!Number.isNaN(alphaVal)) {
const normalizedAlpha = isPercent ? alphaVal / 100 : alphaVal;
return Math.max(0, Math.min(1, normalizedAlpha));
}
}
if (parts.length >= 3) return 1;
return null;
};
const rawAlpha = extractAlpha(raw);
if (rawAlpha !== null) return rawAlpha;
const normalized = normalizeColorToken(raw);
if (typeof normalized === 'string' && normalized !== raw) {
const normalizedAlpha = extractAlpha(normalized);
if (normalizedAlpha !== null) return normalizedAlpha;
}
return null;
}
function rgbFromColor(color) {
const normalized = normalizeColorToken(color);
return hexToRgb(normalized) || parseRgbString(normalized);
@@ -3012,6 +3047,7 @@ function normalizeDatasetColors(payload, chartType) {
}
const type = chartType || 'bar';
const needsArrayColors = type === 'pie' || type === 'doughnut' || type === 'polarArea';
const MIN_PIE_ALPHA = 0.6;
const pickColor = (value, fallback) => {
if (Array.isArray(value) && value.length) return value[0];
return value || fallback;
@@ -3036,13 +3072,29 @@ function normalizeDatasetColors(payload, chartType) {
const dataLength = Array.isArray(dataset.data) ? dataset.data.length : 0;
const total = Math.max(labelCount, rawColors.length, dataLength, 1);
const normalizedColors = [];
let fixedTransparentCount = 0;
for (let i = 0; i < total; i++) {
const fallbackColor = DEFAULT_CHART_COLORS[(idx + i) % DEFAULT_CHART_COLORS.length];
const normalizedColor = liftDarkColor(rawColors[i] || fallbackColor);
const normalizedRaw = normalizeColorToken(rawColors[i]);
const alpha = alphaFromColor(normalizedRaw);
const isInvisible = typeof normalizedRaw === 'string' && normalizedRaw.toLowerCase() === 'transparent';
if (alpha === 0 || isInvisible) {
fixedTransparentCount += 1;
}
const baseColor = (!normalizedRaw || isInvisible) ? fallbackColor : normalizedRaw;
const targetAlpha = alpha === null ? 1 : alpha;
const normalizedColor = ensureAlpha(
liftDarkColor(baseColor),
Math.max(MIN_PIE_ALPHA, targetAlpha)
);
normalizedColors.push(normalizedColor);
}
dataset.backgroundColor = normalizedColors;
changes.push(`dataset${idx}: 标准化扇区颜色(${normalizedColors.length})`);
dataset.borderColor = normalizedColors.map(col => ensureAlpha(liftDarkColor(col), 1));
const changeLabel = fixedTransparentCount
? `dataset${idx}: 修正${fixedTransparentCount}个透明扇区`
: `dataset${idx}: 标准化扇区颜色(${normalizedColors.length})`;
changes.push(changeLabel);
return;
}