ChartPipeline / scripts /migrate_legend_pass3.py
Ray1ee01's picture
Upload folder using huggingface_hub
58e6885 verified
Raw
History Blame Contribute Delete
23.5 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"
DRAW_VERTICAL = ''' chartUtils.legend.draw(svg, groups, {
color: group => groupColors[group],
colorResolver,
colors,
}, {
x: size - 150,
y: 50,
direction: "vertical",
markerShape: "rect",
markerSize: 15,
labelGap: 5,
rowGap: 10,
itemHeight: 25,
fontSize: typography.label.font_size,
fontFamily: typography.label.font_family,
fontWeight: typography.label.font_weight,
textColor: colorResolver.text({ fallback: "#333333" }).value,
});'''
BOXED_RADIAL = ''' chartUtils.legend.boxedVertical(svg, legendItems, {
color: d => getColor(d),
colorResolver,
colors,
}, {
x: legendX,
y: legendY,
padding: legendPadding,
itemHeight: legendItemHeight,
markerSize: 15,
labelGap: 10,
fontSize: 12,
textColor: colorResolver.text({ fallback: "#333333" }).value,
});'''
ROSE_DRAW = ''' chartUtils.legend.draw(svg, groups, {
colorScale,
colorResolver,
colors,
}, {
y: 15,
maxWidth: width - 40,
align: "center",
markerShape: "circle",
markerSize: 12,
labelGap: 5,
itemGap: 15,
rowGap: 5,
fontSize: 12,
textColor: colorResolver.text({ fallback: "#333333" }).value,
});'''
GAUGE_DRAW = ''' chartUtils.legend.draw(svg, categoryData.map(d => d.category), {
colorScale,
colorResolver,
colors,
}, {
y: 15,
maxWidth: width - 40,
align: "center",
markerShape: "circle",
markerSize: 12,
labelGap: 5,
itemGap: 15,
rowGap: 5,
fontSize: 12,
textColor: colorResolver.text({ fallback: "#333" }).value,
});'''
def replace_block(text: str, start: str, end: str, replacement: str) -> str:
i = text.find(start)
if i < 0:
return text
j = text.find(end, i + len(start))
if j < 0:
return text
return text[:i] + replacement + text[j:]
def migrate_labeled_strip(text: str, rel: str) -> str:
if "chartUtils.legend." in text:
return text
markers = [
"// ---------- 11. 创建新样式图例",
"// ---------- 9. 创建新样式图例",
]
start = None
for m in markers:
if m in text:
start = text.find(m)
break
if start is None:
return text
end_markers = ["return svg.node();", "// ---------- 9. 创建主图表组", "// ---------- 工具函数 ----------"]
end = None
for m in end_markers:
pos = text.find(m, start)
if pos > start:
end = pos
break
if end is None:
return text
show_number = "horizontal_group_bar_chart_12" in rel
pattern_fill = show_number
width_expr = "width" if "width" in text[start:end] else "size"
y_expr = "height - margin.bottom + 20" if "legendStartY = height" in text else "size - margin.bottom * 0.8 + 10"
fill = (
"(group, index) => index % 2 === 0 ? \"url(#pattern-sketch-left)\" : \"url(#pattern-sketch-right)\""
if pattern_fill else "group => getGroupColor(group)"
)
wrap = "wrapLabel: (node, label, w) => wrapText(node, label, w, 1.1)," if "function wrapText" in text else ""
num = "showNumber: true," if show_number else ""
font = (
"labelFontFamily: \"'Comic Sans MS', cursive\",\n"
" numberFontFamily: \"'Comic Sans MS', cursive\","
if pattern_fill else
"labelFontFamily: typography.annotation.font_family,\n"
" numberFontFamily: typography.label.font_family,"
)
if not show_number and "grouped_circular" in rel:
font = (
"labelFontFamily: typography.annotation.font_family,\n"
" numberFontFamily: typography.label.font_family,"
)
num = "showNumber: true,"
replacement = (
f" const equalGroupRectWidth = Math.min(innerWidth / groups.length, {width_expr} * 0.8 / groups.length);\n"
f" chartUtils.legend.labeledStrip(svg, groups.map((group, index) => ({{\n"
f" label: group,\n"
f" fill: {fill},\n"
f" }})), {{\n"
f" centerWidth: {width_expr},\n"
f" y: {y_expr},\n"
f" itemWidth: equalGroupRectWidth,\n"
f" {num}\n"
f" {font}\n"
f" labelFontSize: 12,\n"
f" textColor: colorResolver.text({{ fallback: \"#333333\" }}).value,\n"
f" {wrap}\n"
f" }});\n\n"
)
return text[:start] + replacement + text[end:]
def migrate_simple_vertical(text: str) -> str:
if "chartUtils.legend." in text:
return text
pattern = re.compile(
r"\n\s*// 添加图例\s*\n"
r"\s*const legend = svg\.append\(\"g\"\)\s*\n"
r"(?:\s*\.attr\(\"class\", \"other\"\)\s*\n)?"
r"\s*\.attr\(\"transform\", `translate\(\$\{size - 150\}, 50\}\)`\);\s*\n"
r"\s*groups\.forEach\(\(group, i\) => \{\s*\n"
r"[\s\S]*?"
r"\s*\}\);\s*\n",
re.MULTILINE,
)
return pattern.sub("\n" + DRAW_VERTICAL + "\n", text, count=1)
def migrate_radial_boxed(text: str) -> str:
if "chartUtils.legend." in text or "legendItemHeight = 20" not in text:
return text
pattern = re.compile(
r"\n\s*const legend = svg\.append\(\"g\"\)\s*\n"
r"\s*\.attr\(\"transform\", `translate\(\$\{legendX\}, \$\{legendY\}\)`\);\s*\n"
r"\s*// 添加图例背景[\s\S]*?"
r"\s*(?:stackedSeries|legendItems)\.forEach\(\([^)]+\) => \{\s*\n"
r"\s*const legendItem = legend\.append\(\"g\"\)\s*\n"
r"[\s\S]*?"
r"\s*\}\);\s*\n",
re.MULTILINE,
)
return pattern.sub("\n" + BOXED_RADIAL + "\n", text, count=1)
def migrate_step_line(text: str) -> str:
if "chartUtils.legend." in text:
return text
pattern = re.compile(
r"\n\s*// 添加图例 - 整体居中\s*\n"
r"[\s\S]*?"
r"\s*legendGroup\.attr\(\"transform\", `translate\(\$\{\(chartWidth - totalLegendWidth\)/2\}, 60\}`\);\s*\n",
re.MULTILINE,
)
repl = (
"\n chartUtils.legend.stepLineCentered(g, {\n"
" centerX: chartWidth / 2,\n"
" y: 60,\n"
" fieldName: dataColumns[1].label || dataColumns[1].name,\n"
" areaColor,\n"
" });\n"
)
return pattern.sub(repl, text, count=1)
def migrate_bump(text: str) -> str:
if "chartUtils.legend." in text:
return text
pattern = re.compile(
r"\n\s*const legendGroup = g\.append\(\"g\"\)\s*\n"
r"\s*\.attr\(\"class\", \"chart-legend\"\)\s*\n"
r"\s*\.attr\(\"transform\", `translate\(\$\{legendStartX\}, \$\{legendY\}\)`\);\s*\n"
r"\s*let currentLegendX = 0;\s*\n"
r"\s*categoryGroups\.forEach\(\(cg, index\) => \{\s*\n"
r"[\s\S]*?"
r"\s*currentLegendX \+= itemWidth \+ legendColumnPadding;\s*\n"
r"\s*\}\);\s*\n",
re.MULTILINE,
)
repl = (
"\n chartUtils.legend.draw(g, categoryGroups, {\n"
" color: cg => colorScale(cg),\n"
" colorResolver,\n"
" colors,\n"
" }, {\n"
" x: legendStartX,\n"
" y: legendY,\n"
" maxWidth: innerWidth,\n"
" fitToWidth: true,\n"
" minFontSize: 8,\n"
" markerShape: \"rect\",\n"
" markerSize: legendSquareSize,\n"
" labelGap: legendItemPadding,\n"
" itemGap: legendColumnPadding,\n"
" fontSize: finalLegendFontSize,\n"
" fontFamily: legendFontFamily,\n"
" fontWeight: legendFontWeight,\n"
" textColor: legendColor,\n"
" });\n"
)
return pattern.sub(repl, text, count=1)
def migrate_rose(text: str) -> str:
if "chartUtils.legend." in text or "legendConfig.maxWidth" not in text:
return text
pattern = re.compile(
r"\n\s*const legendGroup = svg\.append\(\"g\"\)\.attr\(\"class\", \"other\"\);\s*\n"
r"\s*const legendConfig = \{[\s\S]*?\n\s*tempText\.remove\(\);\s*\n",
re.MULTILINE,
)
return pattern.sub("\n" + ROSE_DRAW + "\n", text, count=1)
def migrate_gauge_sm(text: str) -> str:
if "chartUtils.legend." in text or "legendConfig.maxWidth" not in text:
return text
pattern = re.compile(
r"\n\s*const legendGroup = svg\.append\(\"g\"\)\.attr\(\"class\", \"other\"\);\s*\n"
r"\s*const legendConfig = \{[\s\S]*?\n\s*tempText\.remove\(\);\s*\n",
re.MULTILINE,
)
return pattern.sub("\n" + GAUGE_DRAW + "\n", text, count=1)
def migrate_type18(text: str) -> str:
if "chartUtils.legend." in text:
return text
pattern = re.compile(
r"\n\s*const legendY = margin\.top - 30;[\s\S]*?"
r"\s*currentX \+= legendWidths\[i\] \+ \(i < legendData\.length - 1 \? 5 : 0\);\s*\n"
r"\s*\}\);\s*\n",
re.MULTILINE,
)
repl = (
"\n const legendY = margin.top - 30;\n"
" chartUtils.legend.segmentsCentered(svg, [\n"
" { type: \"rect\", size: 12, color: colorScale(categories[0]) },\n"
" { type: \"rect\", size: 12, color: colorScale(categories[1]) },\n"
" { type: \"text\", label: valueField },\n"
" { type: \"circle\", size: 12, radius: 6, color: colorScale(categories[0]) },\n"
" { type: \"text\", label: valueField2 },\n"
" ], {\n"
" centerX: margin.left + innerWidth / 2,\n"
" y: legendY,\n"
" itemGap: 5,\n"
" sectionGap: 5,\n"
" fontSize: 12,\n"
" fontFamily: typography.annotation.font_family,\n"
" textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n"
" });\n"
)
return pattern.sub(repl, text, count=1)
def migrate_vg7(text: str) -> str:
if "chartUtils.legend." in text:
return text
pattern = re.compile(
r"\n\s*const legendY = margin\.top / 4\*3;[\s\S]*?"
r"\s*\.text\(secondaryMetricName\);\s*\n",
re.MULTILINE,
)
s = "legendSquareSize"
repl = (
f"\n const legendY = margin.top / 4 * 3;\n"
f" const legendSquareSize = 12;\n"
f" const swatchPath = size => [\n"
f" `M0,0 L${{size}},0 L0,${{size}} Z`,\n"
f" `M${{size}},${{size}} L0,${{size}} L${{size}},0 Z`,\n"
f" ];\n"
f" chartUtils.legend.segmentsCentered(svg, [\n"
f" {{ type: \"path\", size: legendSquareSize, d: swatchPath(legendSquareSize)[0], color: legendG1Color1, transform: `translate(0, ${{-legendSquareSize / 2}})` }},\n"
f" {{ type: \"path\", size: 0, d: swatchPath(legendSquareSize)[1], color: legendG1Color2, transform: `translate(0, ${{-legendSquareSize / 2}})` }},\n"
f" {{ type: \"text\", label: primaryMetricName }},\n"
f" {{ type: \"path\", size: legendSquareSize, d: swatchPath(legendSquareSize)[0], color: legendG1Color1, transform: `translate(0, ${{-legendSquareSize / 2}})` }},\n"
f" {{ type: \"path\", size: 0, d: swatchPath(legendSquareSize)[1], color: legendG1Color2, transform: `translate(0, ${{-legendSquareSize / 2}})` }},\n"
f" {{ type: \"text\", label: secondaryMetricName }},\n"
f" ], {{\n"
f" centerX: margin.left + innerWidth / 2,\n"
f" y: legendY,\n"
f" itemGap: legendItemPadding,\n"
f" sectionGap: legendPadding,\n"
f" fontSize: legendFontSize,\n"
f" fontFamily: legendFontFamily,\n"
f" fontWeight: legendFontWeight,\n"
f" textColor: colorResolver.text({{ fallback: \"#000000\" }}).value,\n"
f" }});\n"
)
return pattern.sub(repl, text, count=1)
def migrate_scatter(text: str) -> str:
if "chartUtils.legend." in text:
return text
pattern = re.compile(
r"\n\s*const legendGroup = svg\.append\(\"g\"\)\.attr\(\"class\", \"chart-legend\"\);\s*\n"
r"[\s\S]*?"
r"\s*currentX \+= legendSquareSize \+ legendItemPadding \+ textWidth \+ legendColumnPadding;\s*\n"
r"\s*\}\);\s*\n",
re.MULTILINE,
)
repl = (
"\n const legendTitleText = groupField;\n"
" const titleWidth = chartUtils.text.measure(svg, legendTitleText, {\n"
" fontSize: legendFontSize + 1,\n"
" fontFamily: legendFontFamily,\n"
" fontWeight: \"bold\",\n"
" }).width;\n"
" const legendDraw = chartUtils.legend.draw(svg, groups, {\n"
" color: group => colorScale(group),\n"
" colorResolver,\n"
" colors,\n"
" }, {\n"
" x: margin.left + titleWidth + 15,\n"
" y: legendY,\n"
" markerShape: \"rect\",\n"
" markerSize: legendSquareSize,\n"
" labelGap: legendItemPadding,\n"
" itemGap: legendColumnPadding,\n"
" fontSize: legendFontSize,\n"
" fontFamily: legendFontFamily,\n"
" fontWeight: legendFontWeight,\n"
" textColor: legendColor,\n"
" });\n"
" const legendBgWidth = titleWidth + 15 + legendDraw.width + 20;\n"
" const legendBgHeight = legendSquareSize + 20;\n"
" const legendBgX = margin.left - 10;\n"
" const legendBgY = legendY - legendBgHeight / 2;\n"
" const legendGroup = svg.insert(\"g\", \".chart-legend\").attr(\"class\", \"chart-legend\");\n"
" legendGroup.append(\"rect\")\n"
" .attr(\"class\", \"legend-background\")\n"
" .attr(\"x\", legendBgX)\n"
" .attr(\"y\", legendBgY)\n"
" .attr(\"width\", legendBgWidth)\n"
" .attr(\"height\", legendBgHeight)\n"
" .attr(\"fill\", \"white\")\n"
" .attr(\"stroke\", \"#ddd\")\n"
" .attr(\"rx\", 4)\n"
" .attr(\"ry\", 4);\n"
" legendGroup.append(\"text\")\n"
" .attr(\"class\", \"legend-title\")\n"
" .attr(\"x\", margin.left)\n"
" .attr(\"y\", legendY)\n"
" .attr(\"dominant-baseline\", \"middle\")\n"
" .style(\"font-family\", legendFontFamily)\n"
" .style(\"font-size\", `${legendFontSize + 1}px`)\n"
" .style(\"font-weight\", \"bold\")\n"
" .style(\"fill\", legendColor)\n"
" .text(legendTitleText);\n"
)
return pattern.sub(repl, text, count=1)
def migrate_proportional(text: str) -> str:
if "chartUtils.legend." in text or "legendG.append" not in text:
return text
pattern = re.compile(
r"\n\s*const legendG = svg\.append\(\"g\"\)\s*\n"
r"\s*\.attr\(\"class\", \"legend\"\)\s*\n"
r"\s*\.attr\(\"transform\", `translate\(\$\{legendMargin\.left\}, \$\{legendMargin\.top\}\)`\);\s*\n"
r"[\s\S]*?"
r"\s*legendItems\.forEach\(\(item, i\) => \{\s*\n"
r"[\s\S]*?"
r"\s*\}\);\s*\n",
re.MULTILINE,
)
repl = (
"\n const legendItemsForSide = uniqueCategories.map(category => ({\n"
" label: category,\n"
" color: categoryColors[category],\n"
" raw: {\n"
" imageHref: images.field && images.field[category] ? images.field[category] : null,\n"
" },\n"
" }));\n"
" chartUtils.legend.sideDetail(svg, legendItemsForSide, {\n"
" colorResolver,\n"
" colors,\n"
" }, {\n"
" x: legendMargin.left,\n"
" y: legendMargin.top,\n"
" fontSize: legendFontSize,\n"
" fontFamily: legendFontFamily,\n"
" textColor: colorResolver.text({ fallback: \"#333333\" }).value,\n"
" });\n"
)
return pattern.sub(repl, text, count=1)
def migrate_stacked_area(text: str) -> str:
if "chartUtils.legend." in text:
return text
pattern = re.compile(
r"\n\s*const legendGroup = g\.append\(\"g\"\)\s*\n"
r"[\s\S]*?"
r"\s*legendGroup\.attr\(\"transform\", `translate\(\$\{\(chartWidth - totalLegendWidth\)/2\}, 60\}`\);\s*\n",
re.MULTILINE,
)
repl = (
"\n chartUtils.legend.draw(g, groups, {\n"
" color: group => getColor(group),\n"
" colorResolver,\n"
" colors,\n"
" }, {\n"
" x: (chartWidth - chartUtils.legend.layout(groups, { color: group => getColor(group), colorResolver, colors, context: g, markerShape: \"line\", markerSize: 30 }).width) / 2,\n"
" y: 60,\n"
" markerShape: \"line\",\n"
" markerSize: 30,\n"
" labelGap: 10,\n"
" itemGap: 20,\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 pattern.sub(repl, text, count=1)
def migrate_line_box(text: str) -> str:
if "chartUtils.legend." in text:
return text
pattern = re.compile(
r"\n\s*const legend = svg\.append\(\"g\"\)\s*\n"
r"\s*\.attr\(\"transform\", `translate\(\$\{legendX\}, \$\{legendY\}\)`\);\s*\n"
r"\s*legend\.append\(\"rect\"\)[\s\S]*?"
r"\s*legendItems\.forEach\(\(item, i\) => \{\s*\n"
r"[\s\S]*?"
r"\s*\}\);\s*\n",
re.MULTILINE,
)
repl = (
"\n const legendLayout = chartUtils.legend.layout(legendItems, {\n"
" color: item => getColor(item),\n"
" colorResolver,\n"
" colors,\n"
" context: svg,\n"
" direction: \"vertical\",\n"
" markerShape: \"line\",\n"
" markerSize: legendLineWidth,\n"
" labelGap: legendTextOffset,\n"
" rowGap: legendItemHeight,\n"
" itemHeight: legendItemHeight,\n"
" fontSize: typography.label.font_size,\n"
" fontFamily: typography.label.font_family,\n"
" });\n"
" const legend = svg.append(\"g\").attr(\"transform\", `translate(${legendX}, ${legendY})`);\n"
" legend.append(\"rect\")\n"
" .attr(\"width\", legendWidth)\n"
" .attr(\"height\", legendHeight)\n"
" .attr(\"fill\", \"white\")\n"
" .attr(\"stroke\", \"#ccc\")\n"
" .attr(\"opacity\", 0.9);\n"
" chartUtils.legend.draw(legend, legendItems, {\n"
" color: item => getColor(item),\n"
" colorResolver,\n"
" colors,\n"
" }, {\n"
" x: legendPadding,\n"
" y: legendPadding,\n"
" direction: \"vertical\",\n"
" markerShape: \"line\",\n"
" markerSize: legendLineWidth,\n"
" labelGap: legendTextOffset,\n"
" rowGap: legendItemSpacing,\n"
" itemHeight: legendItemHeight,\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 pattern.sub(repl, text, count=1)
def migrate_vg4(text: str) -> str:
if "chartUtils.legend." in text:
return text
if "legendItems.find" not in text:
return text
start = text.find("// 添加图例")
if start < 0:
start = text.find("const legendItems = [];")
end = text.find("// ---------- 8.", start)
if end < 0:
end = text.find("return svg.node();", start)
if start < 0 or end < 0:
return text
repl = (
" chartUtils.legend.draw(svg, legendItems.map(item => item.label), {\n"
" color: (_, i) => legendItems[i].color,\n"
" colorResolver,\n"
" colors,\n"
" }, {\n"
" x: width - legendTotalWidth,\n"
" y: margin.top - 50,\n"
" direction: \"vertical\",\n"
" markerShape: \"rect\",\n"
" markerSize: legendRectSize,\n"
" labelGap: spaceBetweenRectAndText,\n"
" rowGap: legendItemHeight - legendRectSize,\n"
" itemHeight: legendItemHeight,\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\n"
)
pre = text[:start]
post = text[end:]
pre = re.sub(
r"\n\s*const legendItems = \[\];\s*\n[\s\S]*?const legendTotalWidth = [^;]+;\s*\n",
"\n const legendItems = [];\n group1Values.forEach(g1 => {\n group2Values.forEach(g2 => {\n legendItems.push({ label: `${g2} / ${g1}`, color: getColor(g1, g2) });\n });\n });\n const legendTotalWidth = 180;\n",
pre,
count=1,
)
return pre + repl + post
def process(path: Path) -> bool:
original = path.read_text(encoding="utf-8")
if "chartUtils.legend." in original:
return False
rel = str(path.relative_to(TEMPLATE_ROOT))
text = original
text = migrate_labeled_strip(text, rel)
text = migrate_simple_vertical(text)
text = migrate_radial_boxed(text)
text = migrate_step_line(text)
text = migrate_bump(text)
text = migrate_rose(text)
text = migrate_gauge_sm(text)
text = migrate_type18(text)
text = migrate_vg7(text)
text = migrate_scatter(text)
text = migrate_proportional(text)
text = migrate_stacked_area(text)
text = migrate_line_box(text)
text = migrate_vg4(text)
if text != original:
path.write_text(text, encoding="utf-8")
return True
return False
def main() -> None:
changed = []
for path in sorted(TEMPLATE_ROOT.rglob("*.js")):
if process(path):
changed.append(str(path.relative_to(TEMPLATE_ROOT)))
print(f"migrated {len(changed)} files")
for f in changed:
print(f)
if __name__ == "__main__":
main()