Showing
2 changed files
with
106 additions
and
17 deletions
| @@ -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 |
-
Please register or login to post a comment