ChartPipeline / scripts /migrate_legend_pass2.py
Ray1ee01's picture
Upload folder using huggingface_hub
58e6885 verified
Raw
History Blame Contribute Delete
19.1 kB
#!/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()