马一丁

Optimize the Color Replacement Scheme for Pie Charts

@@ -278,6 +278,25 @@ class ChartToSVGConverter: @@ -278,6 +278,25 @@ class ChartToSVGConverter:
278 # 其他格式(十六进制、颜色名等)直接返回 278 # 其他格式(十六进制、颜色名等)直接返回
279 return color 279 return color
280 280
  281 + def _ensure_visible_color(self, color: Any, fallback: str, min_alpha: float = 0.6) -> Any:
  282 + """
  283 + 确保颜色在渲染时可见:避免透明值并提升过低的不透明度
  284 + """
  285 + base_color = fallback if color in (None, "", "transparent") else color
  286 + parsed = self._parse_color(base_color)
  287 + fallback_parsed = self._parse_color(fallback)
  288 +
  289 + if isinstance(parsed, tuple):
  290 + if len(parsed) == 4:
  291 + r, g, b, a = parsed
  292 + return (r, g, b, max(a, min_alpha))
  293 + return parsed
  294 +
  295 + if isinstance(parsed, str) and parsed.lower() == "transparent":
  296 + return fallback_parsed
  297 +
  298 + return parsed if parsed is not None else fallback_parsed
  299 +
281 def _get_colors(self, datasets: List[Dict[str, Any]]) -> List[str]: 300 def _get_colors(self, datasets: List[Dict[str, Any]]) -> List[str]:
282 """ 301 """
283 获取图表颜色 302 获取图表颜色
@@ -659,12 +678,17 @@ class ChartToSVGConverter: @@ -659,12 +678,17 @@ class ChartToSVGConverter:
659 fig, ax = self._create_figure(width, height, dpi, title) 678 fig, ax = self._create_figure(width, height, dpi, title)
660 679
661 # 获取颜色 680 # 获取颜色
662 - colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])  
663 - if not isinstance(colors, list):  
664 - colors = self.DEFAULT_COLORS[:len(labels)]  
665 -  
666 - # 【修复】解析每个颜色,将CSS格式转换为matplotlib格式  
667 - colors = [self._parse_color(c) for c in colors] 681 + raw_colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])
  682 + if not isinstance(raw_colors, list):
  683 + raw_colors = self.DEFAULT_COLORS[:len(labels)]
  684 +
  685 + colors = [
  686 + self._ensure_visible_color(
  687 + raw_colors[i] if i < len(raw_colors) else None,
  688 + self.DEFAULT_COLORS[i % len(self.DEFAULT_COLORS)]
  689 + )
  690 + for i in range(len(labels))
  691 + ]
668 692
669 # 绘制饼图 693 # 绘制饼图
670 wedges, texts, autotexts = ax.pie( 694 wedges, texts, autotexts = ax.pie(
@@ -713,12 +737,17 @@ class ChartToSVGConverter: @@ -713,12 +737,17 @@ class ChartToSVGConverter:
713 fig, ax = self._create_figure(width, height, dpi, title) 737 fig, ax = self._create_figure(width, height, dpi, title)
714 738
715 # 获取颜色 739 # 获取颜色
716 - colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])  
717 - if not isinstance(colors, list):  
718 - colors = self.DEFAULT_COLORS[:len(labels)]  
719 -  
720 - # 【修复】解析每个颜色,将CSS格式转换为matplotlib格式  
721 - colors = [self._parse_color(c) for c in colors] 740 + raw_colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])
  741 + if not isinstance(raw_colors, list):
  742 + raw_colors = self.DEFAULT_COLORS[:len(labels)]
  743 +
  744 + colors = [
  745 + self._ensure_visible_color(
  746 + raw_colors[i] if i < len(raw_colors) else None,
  747 + self.DEFAULT_COLORS[i % len(self.DEFAULT_COLORS)]
  748 + )
  749 + for i in range(len(labels))
  750 + ]
722 751
723 # 绘制圆环图(通过设置wedgeprops实现中空效果) 752 # 绘制圆环图(通过设置wedgeprops实现中空效果)
724 wedges, texts, autotexts = ax.pie( 753 wedges, texts, autotexts = ax.pie(
@@ -889,9 +918,17 @@ class ChartToSVGConverter: @@ -889,9 +918,17 @@ class ChartToSVGConverter:
889 ax.set_title(title, fontsize=14, fontweight='bold', pad=20) 918 ax.set_title(title, fontsize=14, fontweight='bold', pad=20)
890 919
891 # 获取颜色 920 # 获取颜色
892 - colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])  
893 - if not isinstance(colors, list):  
894 - colors = self.DEFAULT_COLORS[:len(labels)] 921 + raw_colors = dataset.get('backgroundColor', self.DEFAULT_COLORS[:len(labels)])
  922 + if not isinstance(raw_colors, list):
  923 + raw_colors = self.DEFAULT_COLORS[:len(labels)]
  924 +
  925 + colors = [
  926 + self._ensure_visible_color(
  927 + raw_colors[i] if i < len(raw_colors) else None,
  928 + self.DEFAULT_COLORS[i % len(self.DEFAULT_COLORS)]
  929 + )
  930 + for i in range(len(labels))
  931 + ]
895 932
896 # 计算角度 933 # 计算角度
897 theta = np.linspace(0, 2 * np.pi, len(labels), endpoint=False) 934 theta = np.linspace(0, 2 * np.pi, len(labels), endpoint=False)
@@ -2965,6 +2965,41 @@ function parseRgbString(color) { @@ -2965,6 +2965,41 @@ function parseRgbString(color) {
2965 return [parts[0], parts[1], parts[2]].map(v => Math.max(0, Math.min(255, v))); 2965 return [parts[0], parts[1], parts[2]].map(v => Math.max(0, Math.min(255, v)));
2966 } 2966 }
2967 2967
  2968 +function alphaFromColor(color) {
  2969 + if (typeof color !== 'string') return null;
  2970 + const raw = color.trim();
  2971 + if (!raw) return null;
  2972 + if (raw.toLowerCase() === 'transparent') return 0;
  2973 +
  2974 + const extractAlpha = (source) => {
  2975 + const match = source.match(/rgba?\s*\(([^)]+)\)/i);
  2976 + if (!match) return null;
  2977 + const parts = match[1].split(',').map(p => p.trim());
  2978 + if (source.toLowerCase().startsWith('rgba') && parts.length >= 2) {
  2979 + const alphaToken = parts[parts.length - 1];
  2980 + const isPercent = /%$/.test(alphaToken);
  2981 + const alphaVal = parseFloat(alphaToken.replace('%', ''));
  2982 + if (!Number.isNaN(alphaVal)) {
  2983 + const normalizedAlpha = isPercent ? alphaVal / 100 : alphaVal;
  2984 + return Math.max(0, Math.min(1, normalizedAlpha));
  2985 + }
  2986 + }
  2987 + if (parts.length >= 3) return 1;
  2988 + return null;
  2989 + };
  2990 +
  2991 + const rawAlpha = extractAlpha(raw);
  2992 + if (rawAlpha !== null) return rawAlpha;
  2993 +
  2994 + const normalized = normalizeColorToken(raw);
  2995 + if (typeof normalized === 'string' && normalized !== raw) {
  2996 + const normalizedAlpha = extractAlpha(normalized);
  2997 + if (normalizedAlpha !== null) return normalizedAlpha;
  2998 + }
  2999 +
  3000 + return null;
  3001 +}
  3002 +
2968 function rgbFromColor(color) { 3003 function rgbFromColor(color) {
2969 const normalized = normalizeColorToken(color); 3004 const normalized = normalizeColorToken(color);
2970 return hexToRgb(normalized) || parseRgbString(normalized); 3005 return hexToRgb(normalized) || parseRgbString(normalized);
@@ -3012,6 +3047,7 @@ function normalizeDatasetColors(payload, chartType) { @@ -3012,6 +3047,7 @@ function normalizeDatasetColors(payload, chartType) {
3012 } 3047 }
3013 const type = chartType || 'bar'; 3048 const type = chartType || 'bar';
3014 const needsArrayColors = type === 'pie' || type === 'doughnut' || type === 'polarArea'; 3049 const needsArrayColors = type === 'pie' || type === 'doughnut' || type === 'polarArea';
  3050 + const MIN_PIE_ALPHA = 0.6;
3015 const pickColor = (value, fallback) => { 3051 const pickColor = (value, fallback) => {
3016 if (Array.isArray(value) && value.length) return value[0]; 3052 if (Array.isArray(value) && value.length) return value[0];
3017 return value || fallback; 3053 return value || fallback;
@@ -3036,13 +3072,29 @@ function normalizeDatasetColors(payload, chartType) { @@ -3036,13 +3072,29 @@ function normalizeDatasetColors(payload, chartType) {
3036 const dataLength = Array.isArray(dataset.data) ? dataset.data.length : 0; 3072 const dataLength = Array.isArray(dataset.data) ? dataset.data.length : 0;
3037 const total = Math.max(labelCount, rawColors.length, dataLength, 1); 3073 const total = Math.max(labelCount, rawColors.length, dataLength, 1);
3038 const normalizedColors = []; 3074 const normalizedColors = [];
  3075 + let fixedTransparentCount = 0;
3039 for (let i = 0; i < total; i++) { 3076 for (let i = 0; i < total; i++) {
3040 const fallbackColor = DEFAULT_CHART_COLORS[(idx + i) % DEFAULT_CHART_COLORS.length]; 3077 const fallbackColor = DEFAULT_CHART_COLORS[(idx + i) % DEFAULT_CHART_COLORS.length];
3041 - const normalizedColor = liftDarkColor(rawColors[i] || fallbackColor); 3078 + const normalizedRaw = normalizeColorToken(rawColors[i]);
  3079 + const alpha = alphaFromColor(normalizedRaw);
  3080 + const isInvisible = typeof normalizedRaw === 'string' && normalizedRaw.toLowerCase() === 'transparent';
  3081 + if (alpha === 0 || isInvisible) {
  3082 + fixedTransparentCount += 1;
  3083 + }
  3084 + const baseColor = (!normalizedRaw || isInvisible) ? fallbackColor : normalizedRaw;
  3085 + const targetAlpha = alpha === null ? 1 : alpha;
  3086 + const normalizedColor = ensureAlpha(
  3087 + liftDarkColor(baseColor),
  3088 + Math.max(MIN_PIE_ALPHA, targetAlpha)
  3089 + );
3042 normalizedColors.push(normalizedColor); 3090 normalizedColors.push(normalizedColor);
3043 } 3091 }
3044 dataset.backgroundColor = normalizedColors; 3092 dataset.backgroundColor = normalizedColors;
3045 - changes.push(`dataset${idx}: 标准化扇区颜色(${normalizedColors.length})`); 3093 + dataset.borderColor = normalizedColors.map(col => ensureAlpha(liftDarkColor(col), 1));
  3094 + const changeLabel = fixedTransparentCount
  3095 + ? `dataset${idx}: 修正${fixedTransparentCount}个透明扇区`
  3096 + : `dataset${idx}: 标准化扇区颜色(${normalizedColors.length})`;
  3097 + changes.push(changeLabel);
3046 return; 3098 return;
3047 } 3099 }
3048 3100