Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| from pathlib import Path | |
| import re | |
| ROOT = Path(__file__).resolve().parents[1] | |
| TEMPLATE_ROOT = ROOT / "modules/chart_engine/template/d3-js" | |
| def subn(pattern: str, repl, text: str, count: int = 0) -> tuple[str, int]: | |
| return re.subn(pattern, repl, text, count=count, flags=re.MULTILINE) | |
| def replace_capsule_rows(text: str) -> tuple[str, int]: | |
| if "legendCapsuleHeightH" not in text or "legendRows.forEach" not in text: | |
| return text, 0 | |
| if "chartUtils.legend.draw" in text and "markerShape: \"capsule\"" in text: | |
| return text, 0 | |
| pattern = ( | |
| r"\n\s*const legendGroup = svg\.append\(\"g\"\)\s*\n" | |
| r"\s*\.attr\(\"class\", \"legend\"\)[\s\S]*?" | |
| r"\s*currentY \+= rowHeight;\s*\n" | |
| r"\s*\}\);\s*\n" | |
| ) | |
| replacement = ( | |
| "\n chartUtils.legend.draw(svg, groups, {\n" | |
| " color: (group, index) => `url(#pattern-${group.replace(/[^a-zA-Z0-9]/g, '-')}-${index})`,\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " }, {\n" | |
| " y: margin.top * 0.7,\n" | |
| " maxWidth: width - 100,\n" | |
| " align: \"center\",\n" | |
| " markerShape: \"capsule\",\n" | |
| " markerWidth: legendCapsuleWidthH || 15,\n" | |
| " markerHeight: legendCapsuleHeightH || 10,\n" | |
| " labelGap: legendTextPadding || 5,\n" | |
| " itemGap: legendItemPadding || 15,\n" | |
| " rowGap: legendVerticalSpacing || 10,\n" | |
| " fontSize: typography.label.font_size,\n" | |
| " fontFamily: typography.label.font_family,\n" | |
| " fontWeight: typography.label.font_weight,\n" | |
| " textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n" | |
| " });\n" | |
| ) | |
| text2, n = subn(pattern, replacement, text, count=1) | |
| if n: | |
| text2 = re.sub( | |
| r"\n\s*const legendItemsData = \[[\s\S]*?tempLegendSvg\.remove\(\);\s*\n", | |
| "\n", | |
| text2, | |
| count=1, | |
| ) | |
| text2 = re.sub( | |
| r"\n\s*const legendRows = \[[\s\S]*?legendRows\.push\(currentRow\);\s*\n\s*\}\s*\n", | |
| "\n", | |
| text2, | |
| count=1, | |
| ) | |
| return text2, n | |
| def replace_multiple_pie_legend(text: str) -> tuple[str, int]: | |
| if "legendRows.forEach" not in text or "allGroups.forEach" not in text: | |
| return text, 0 | |
| if "chartUtils.legend." in text: | |
| return text, 0 | |
| pattern = ( | |
| r"\n\s*// ---------- 5\.1 创建图例 ----------\s*\n" | |
| r"[\s\S]*?" | |
| r"\s*currentX \+= itemWidth;\s*\n" | |
| r"\s*\}\);\s*\n" | |
| r"\s*\}\);\s*\n" | |
| ) | |
| replacement = ( | |
| "\n const allGroups = Array.from(new Set(chartData.map(d => d[groupField])));\n" | |
| " const legendLayoutInfo = chartUtils.legend.layout(allGroups, {\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " maxWidth: width - 40,\n" | |
| " align: \"center\",\n" | |
| " context: svg,\n" | |
| " fontSize: typography.annotation.font_size,\n" | |
| " fontFamily: typography.annotation.font_family,\n" | |
| " fontWeight: typography.annotation.font_weight,\n" | |
| " });\n" | |
| " margin.top = legendLayoutInfo.height + 20;\n" | |
| " chartUtils.legend.draw(svg, allGroups, {\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " }, {\n" | |
| " x: margin.left,\n" | |
| " y: 10,\n" | |
| " maxWidth: width - 40,\n" | |
| " align: \"center\",\n" | |
| " markerShape: \"circle\",\n" | |
| " markerSize: 15,\n" | |
| " labelGap: 5,\n" | |
| " itemGap: 15,\n" | |
| " rowGap: 12,\n" | |
| " fontSize: typography.annotation.font_size,\n" | |
| " fontFamily: typography.annotation.font_family,\n" | |
| " fontWeight: typography.annotation.font_weight,\n" | |
| " textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n" | |
| " });\n" | |
| ) | |
| return subn(pattern, replacement, text, count=1) | |
| def replace_stacked_bar_legend_block(text: str) -> tuple[str, int]: | |
| if "calculateLegendItemWidths" not in text: | |
| return text, 0 | |
| pattern = ( | |
| r"\n\s*// ---------- 1[34]\. 创建图例[\s\S]*?" | |
| r"(?:\s*tempSvg\.remove\(\);\s*\n)?" | |
| r"(?=\s*// ----------)" | |
| ) | |
| replacement = ( | |
| "\n chartUtils.legend.draw(svg, groups, {\n" | |
| " color: group => getColor(group),\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " }, {\n" | |
| " x: margin.left,\n" | |
| " y: margin.top - 30,\n" | |
| " maxWidth: barChartWidth,\n" | |
| " fitToWidth: true,\n" | |
| " minFontSize: 8,\n" | |
| " markerShape: \"rect\",\n" | |
| " markerSize: 12,\n" | |
| " labelGap: 5,\n" | |
| " itemGap: 15,\n" | |
| " fontSize: typography.label.font_size,\n" | |
| " fontFamily: typography.label.font_family,\n" | |
| " fontWeight: typography.label.font_weight,\n" | |
| " textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n" | |
| " });\n" | |
| ) | |
| return subn(pattern, replacement, text, count=1) | |
| def replace_proportional_rows_legend(text: str) -> tuple[str, int]: | |
| if "const rows = []" not in text or "uniqueGroupsForLegend.forEach" not in text: | |
| return text, 0 | |
| if "legend-grad-" in text: | |
| return text, 0 | |
| pattern = ( | |
| r"\n\s*/\* ============ 8\. 创建图例 ============ \*/\s*\n" | |
| r"[\s\S]*?" | |
| r"(?=\s*return svg\.node\(\);)" | |
| ) | |
| replacement = ( | |
| "\n const uniqueGroupsForLegend = uniqueGroups;\n" | |
| " chartUtils.legend.draw(svg, uniqueGroupsForLegend, {\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " colorScale,\n" | |
| " }, {\n" | |
| " x: margin.left,\n" | |
| " y: 20,\n" | |
| " maxWidth: W - 40,\n" | |
| " align: \"center\",\n" | |
| " markerShape: \"circle\",\n" | |
| " markerSize: 12,\n" | |
| " labelGap: 8,\n" | |
| " itemGap: 20,\n" | |
| " rowGap: 10,\n" | |
| " fontSize: 12,\n" | |
| " fontFamily: \"Arial\",\n" | |
| " });\n" | |
| ) | |
| return subn(pattern, replacement, text, count=1) | |
| def replace_proportional_gradient_legend(text: str) -> tuple[str, int]: | |
| if "legend-grad-" not in text or "uniqueGroupsForLegend.forEach" not in text: | |
| return text, 0 | |
| pattern = ( | |
| r"\n\s*/\* ============ 8\. 创建图例 ============ \*/\s*\n" | |
| r"[\s\S]*?" | |
| r"\s*\.text\(group\);\s*\n" | |
| r"\s*\}\);\s*\n" | |
| r"(?=\s*return svg\.node\(\);)" | |
| ) | |
| replacement = ( | |
| "\n const uniqueGroupsForLegend = [...new Set(raw.map(d => d[groupField]))];\n" | |
| " const legendTitle = groupCol?.description || \"Categories\";\n" | |
| " const legendTitleG = svg.append(\"g\")\n" | |
| " .attr(\"class\", \"legend-title\")\n" | |
| " .attr(\"transform\", `translate(${margin.left}, 20)`);\n" | |
| " legendTitleG.append(\"text\")\n" | |
| " .attr(\"x\", 0).attr(\"y\", 10)\n" | |
| " .attr(\"font-size\", \"14px\").attr(\"font-weight\", \"bold\").attr(\"font-family\", \"Arial\")\n" | |
| " .text(legendTitle);\n" | |
| " const gradientLegendItems = chartUtils.legend.withGradientFills(\n" | |
| " defs, uniqueGroupsForLegend, colorScale, colorResolver\n" | |
| " );\n" | |
| " chartUtils.legend.draw(svg, gradientLegendItems, {}, {\n" | |
| " x: margin.left,\n" | |
| " y: 30,\n" | |
| " maxWidth: W,\n" | |
| " align: \"center\",\n" | |
| " markerShape: \"circle\",\n" | |
| " markerSize: 16,\n" | |
| " labelGap: 9,\n" | |
| " itemGap: 15,\n" | |
| " rowGap: 10,\n" | |
| " fontSize: 12,\n" | |
| " fontFamily: \"Arial\",\n" | |
| " });\n" | |
| ) | |
| return subn(pattern, replacement, text, count=1) | |
| def replace_legend_group_offset(text: str) -> tuple[str, int]: | |
| if "chartUtils.legend." in text: | |
| return text, 0 | |
| pattern = ( | |
| r"\n\s*const legendGroup = svg\.append\(\"g\"\)\s*\n" | |
| r"\s*\.attr\(\"transform\", `translate\(\$\{margin\.left\}, \$\{margin\.top - 30\}\)`\);\s*\n" | |
| r"\s*let currentX = 0;\s*\n" | |
| r"\s*groups\.forEach\(\(group, i\) => \{\s*\n" | |
| r"\s*const legendItem = legendGroup\.append\(\"g\"\)\s*\n" | |
| r"[\s\S]*?" | |
| r"\s*currentX \+= legendItemWidths\[i\] \+ spacingBetweenItems;\s*\n" | |
| r"\s*\}\);\s*\n" | |
| r"\s*tempSvg\.remove\(\);\s*\n" | |
| ) | |
| if "spacingBetweenItems" not in text: | |
| return text, 0 | |
| replacement = ( | |
| "\n chartUtils.legend.draw(svg, groups, {\n" | |
| " color: group => getColor(group),\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " }, {\n" | |
| " x: margin.left,\n" | |
| " y: margin.top - 30,\n" | |
| " maxWidth: barChartWidth,\n" | |
| " fitToWidth: true,\n" | |
| " minFontSize: 8,\n" | |
| " markerShape: \"rect\",\n" | |
| " markerSize: 12,\n" | |
| " labelGap: 5,\n" | |
| " itemGap: 15,\n" | |
| " fontSize: typography.label.font_size,\n" | |
| " fontFamily: typography.label.font_family,\n" | |
| " fontWeight: typography.label.font_weight,\n" | |
| " textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n" | |
| " });\n" | |
| ) | |
| return subn(pattern, replacement, text, count=1) | |
| def replace_vertical_bar_simple(text: str) -> tuple[str, int]: | |
| if "chartUtils.legend." in text: | |
| return text, 0 | |
| pattern = ( | |
| r"\n\s*//[^\n]*图例[^\n]*\n" | |
| r"\s*const legend = svg\.append\(\"g\"\)\s*\n" | |
| r"\s*\.attr\(\"class\", \"legend\"\)\s*\n" | |
| r"\s*\.attr\(\"transform\", `translate\(\$\{[^`]+\}, \$\{[^`]+\}\)`\);\s*\n" | |
| r"\s*(?:let )?legendOffset = 0;\s*\n" | |
| r"\s*\w+\.forEach\(\([^)]+\) => \{\s*\n" | |
| r"\s*const legendItem = legend\.append\(\"g\"\)\s*\n" | |
| r"[\s\S]*?" | |
| r"\s*legendOffset \+= [^;]+;\s*\n" | |
| r"\s*\}\);\s*\n" | |
| ) | |
| match = re.search(pattern, text) | |
| if not match: | |
| return text, 0 | |
| block = match.group(0) | |
| gm = re.search(r"(\w+)\.forEach\(\((\w+)", block) | |
| if not gm: | |
| return text, 0 | |
| groups = gm.group(1) | |
| tm = re.search(r"translate\(\$\{([^}]+)\}, \$\{([^}]+)\}\)", block) | |
| if not tm: | |
| return text, 0 | |
| cx, cy = tm.group(1).strip(), tm.group(2).strip() | |
| color = "colorScale,\n colorResolver,\n colors," | |
| if "getColor(" in block: | |
| color = "color: d => getColor(d),\n colorResolver,\n colors," | |
| replacement = ( | |
| f"\n chartUtils.legend.centered(svg, {groups}, {{\n" | |
| f" {color}\n" | |
| f" }}, {cx}, {cy}, {{\n" | |
| f" markerShape: \"rect\",\n" | |
| f" markerSize: 12,\n" | |
| f" labelGap: 5,\n" | |
| f" itemGap: 15,\n" | |
| f" fontSize: typography.label.font_size,\n" | |
| f" fontFamily: typography.label.font_family,\n" | |
| f" fontWeight: typography.label.font_weight,\n" | |
| f" textColor: colorResolver.text({{ fallback: \"#333333\" }}).value,\n" | |
| f" }});\n" | |
| ) | |
| return text[: match.start()] + replacement + text[match.end() :], 1 | |
| def replace_capsule_foreach(text: str) -> tuple[str, int]: | |
| if "legendCapsuleHeightH" not in text or "legendRows.forEach" in text: | |
| return text, 0 | |
| if "chartUtils.legend.draw" in text and "capsule" in text: | |
| return text, 0 | |
| pattern = ( | |
| r"\n\s*// 布局图例项\s*\n" | |
| r"\s*let currentX = [\s\S]*?" | |
| r"\s*currentX \+= itemWidth \+ legendPadding;[^\n]*\n" | |
| r"\s*\}\);\s*\n" | |
| ) | |
| if "legendCapsuleHeightH" not in text: | |
| return text, 0 | |
| y_m = re.search(r"translate\(\$\{margin\.left\}, \$\{([^}]+)\}\)", text) | |
| y_expr = y_m.group(1).strip() if y_m else "margin.top * 0.7" | |
| replacement = ( | |
| "\n chartUtils.legend.draw(svg, groups, {\n" | |
| " color: (group, index) => `url(#pattern-${group.replace(/[^a-zA-Z0-9]/g, '-')}-${index})`,\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " }, {\n" | |
| f" x: margin.left,\n" | |
| f" y: {y_expr},\n" | |
| " maxWidth: width - 100,\n" | |
| " markerShape: \"capsule\",\n" | |
| " markerWidth: legendCapsuleWidthH,\n" | |
| " markerHeight: legendCapsuleHeightH,\n" | |
| " labelGap: legendTextPadding,\n" | |
| " itemGap: legendPadding,\n" | |
| " rowGap: legendVerticalSpacing || 10,\n" | |
| " fontSize: typography.label.font_size,\n" | |
| " fontFamily: typography.label.font_family,\n" | |
| " fontWeight: typography.label.font_weight,\n" | |
| " textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n" | |
| " });\n" | |
| ) | |
| text2, n = subn(pattern, replacement, text, count=1) | |
| if n: | |
| text2 = re.sub( | |
| r"\n\s*const tempLegendSvg = d3\.select\(containerSelector\)[\s\S]*?tempLegendSvg\.remove\(\);\s*\n", | |
| "\n", | |
| text2, | |
| count=1, | |
| ) | |
| text2 = re.sub( | |
| r"\n\s*// 图例项样式参数[\s\S]*?const rowHeight = [^;]+;\s*\n", | |
| "\n", | |
| text2, | |
| count=1, | |
| ) | |
| return text2, n | |
| def replace_legend_other_centered(text: str) -> tuple[str, int]: | |
| pattern = ( | |
| r"\n\s*const legend = svg\.append\(\"g\"\)\s*\n" | |
| r"\s*\.attr\(\"class\", \"other\"\)\s*\n" | |
| r"\s*\.attr\(\"transform\", `translate\(\$\{width / 2\}, \$\{margin\.top / 2\}\)`\);\s*\n" | |
| r"\s*let legendOffset = 0;\s*\n" | |
| r"\s*groups\.forEach\(\(group, i\) => \{\s*\n" | |
| r"[\s\S]*?" | |
| r"\s*legendOffset \+= legendItemWidths\[i\];\s*\n" | |
| r"\s*\}\);\s*\n" | |
| r"\s*legend\.attr\(\"transform\", `translate\(\$\{\(width - totalLegendWidth\) / 2\}, \$\{margin\.top / 2\}\)`\);\s*\n" | |
| ) | |
| replacement = ( | |
| "\n chartUtils.legend.centered(svg, groups, {\n" | |
| " colorScale,\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " }, width / 2, margin.top / 2, {\n" | |
| " markerShape: \"rect\",\n" | |
| " markerSize: 15,\n" | |
| " labelGap: 5,\n" | |
| " itemGap: 10,\n" | |
| " fontSize: typography.label.font_size,\n" | |
| " fontFamily: typography.label.font_family,\n" | |
| " fontWeight: typography.label.font_weight,\n" | |
| " textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n" | |
| " markerRadius: variables.has_rounded_corners ? 2 : 0,\n" | |
| " });\n" | |
| ) | |
| text2, n = subn(pattern, replacement, text, count=1) | |
| if n: | |
| text2 = re.sub( | |
| r"\n\s*// 计算图例项宽度[\s\S]*?tempSvg\.remove\(\);\s*\n", | |
| "\n", | |
| text2, | |
| count=1, | |
| ) | |
| return text2, n | |
| def replace_display_groups_legend(text: str) -> tuple[str, int]: | |
| if "displayGroups.forEach" not in text or "legendItems.push" not in text: | |
| return text, 0 | |
| pattern = ( | |
| r"\n\s*//\s*7\.添加组图例\s*\n" | |
| r"[\s\S]*?" | |
| r"\s*// 如果仍然不适合[\s\S]*?\}\s*\n" | |
| r"\s*\}\s*\n" | |
| ) | |
| replacement = ( | |
| "\n chartUtils.legend.draw(svg, displayGroups, {\n" | |
| " color: group => getColor(group),\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " }, {\n" | |
| " x: margin.left,\n" | |
| " y: margin.top - 20,\n" | |
| " maxWidth: innerWidth + margin.right - 20,\n" | |
| " fitToWidth: true,\n" | |
| " minFontSize: 8,\n" | |
| " markerShape: \"rect\",\n" | |
| " markerSize: legendSquareSize,\n" | |
| " labelGap: legendSpacing,\n" | |
| " itemGap: legendGap,\n" | |
| " fontSize: typography.label.font_size,\n" | |
| " fontFamily: typography.label.font_family,\n" | |
| " fontWeight: typography.label.font_weight,\n" | |
| " textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n" | |
| " });\n" | |
| ) | |
| return subn(pattern, replacement, text, count=1) | |
| def replace_side_vertical_legend(text: str) -> tuple[str, int]: | |
| pattern = ( | |
| r"\n\s*// Add legend\s*\n" | |
| r"\s*const legend = svg\.append\(\"g\"\)\s*\n" | |
| r"\s*\.attr\(\"transform\", `translate\(\$\{width - margin\.right \+ 20\}, \$\{margin\.top\}\)`\);\s*\n" | |
| r"\s*groupValues\.forEach\(\(group, i\) => \{\s*\n" | |
| r"\s*const legendRow = legend\.append\(\"g\"\)\s*\n" | |
| r"[\s\S]*?" | |
| r"\s*\}\);\s*\n" | |
| ) | |
| replacement = ( | |
| "\n chartUtils.legend.draw(svg, groupValues, {\n" | |
| " color: group => getColor(group),\n" | |
| " colorResolver,\n" | |
| " colors,\n" | |
| " }, {\n" | |
| " x: width - margin.right + 20,\n" | |
| " y: margin.top,\n" | |
| " direction: \"vertical\",\n" | |
| " markerShape: \"rect\",\n" | |
| " markerSize: 15,\n" | |
| " labelGap: 5,\n" | |
| " rowGap: 10,\n" | |
| " itemHeight: 25,\n" | |
| " fontSize: typography.label.font_size,\n" | |
| " fontFamily: typography.label.font_family,\n" | |
| " fontWeight: typography.label.font_weight,\n" | |
| " textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n" | |
| " markerRadius: variables.has_rounded_corners ? 2 : 0,\n" | |
| " });\n" | |
| ) | |
| return subn(pattern, replacement, text, count=1) | |
| def migrate_file(path: Path) -> bool: | |
| original = path.read_text(encoding="utf-8") | |
| if "chartUtils.legend." in original and "legendItem.append" not in original: | |
| return False | |
| text = original | |
| for fn in ( | |
| replace_capsule_rows, | |
| replace_capsule_foreach, | |
| replace_multiple_pie_legend, | |
| replace_stacked_bar_legend_block, | |
| replace_legend_group_offset, | |
| replace_proportional_rows_legend, | |
| replace_proportional_gradient_legend, | |
| replace_vertical_bar_simple, | |
| replace_legend_other_centered, | |
| replace_display_groups_legend, | |
| replace_side_vertical_legend, | |
| ): | |
| text, _ = fn(text) | |
| if text != original: | |
| path.write_text(text, encoding="utf-8") | |
| return True | |
| return False | |
| def main() -> None: | |
| changed = 0 | |
| for path in sorted(TEMPLATE_ROOT.rglob("*.js")): | |
| if migrate_file(path): | |
| changed += 1 | |
| print(f"pass2 migrated {changed} files") | |
| if __name__ == "__main__": | |
| main() | |