@topmindspace/tms-skills 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.en.md +140 -0
- package/README.md +37 -24
- package/package.json +3 -2
- package/top-ppt-html/README.en.md +82 -0
- package/top-ppt-html/README.md +30 -18
- package/top-ppt-html/SKILL.md +25 -15
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +438 -62
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.model.json +229 -31
- package/top-ppt-html/assets/showcase/showcase-charts.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-cover.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-gates.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-modes.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-positioning.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-split.png +0 -0
- package/top-ppt-html/assets/showcase/showcase-toolbar.png +0 -0
- package/top-ppt-html/package-lock.json +2 -2
- package/top-ppt-html/package.json +2 -2
- package/top-ppt-html/scripts/hydrate_charts.py +341 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Fill placeholder chart SVGs from REPORT_MODEL (core types for showcase).
|
|
4
|
+
|
|
5
|
+
Used after render_from_model.py — follows extract_snippet chart patterns
|
|
6
|
+
(bar / hbar / line / area / donut / waterfall / stack). Not a public CLI.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
MODEL_RE = re.compile(r'window\.REPORT_MODEL = (\{[\s\S]*?\});')
|
|
16
|
+
PLACEHOLDER_SVG_RE = re.compile(
|
|
17
|
+
r'<svg class="chart" data-chart="([^"]+)" viewBox="0 0 560 220">\s*'
|
|
18
|
+
r'<!--[\s\S]*?-->\s*'
|
|
19
|
+
r'<text[^>]*>[^<]*见模型数据</text>\s*'
|
|
20
|
+
r'</svg>',
|
|
21
|
+
re.M,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def esc(s) -> str:
|
|
26
|
+
return (str(s)
|
|
27
|
+
.replace('&', '&')
|
|
28
|
+
.replace('<', '<')
|
|
29
|
+
.replace('>', '>')
|
|
30
|
+
.replace('"', '"'))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def svg_bar(labels, values, max_v=None, unit='') -> str:
|
|
34
|
+
max_v = float(max_v or max(values) * 1.15 or 1)
|
|
35
|
+
n = len(labels)
|
|
36
|
+
gap, left, right, base, top = 24, 60, 540, 170, 30
|
|
37
|
+
usable = right - left
|
|
38
|
+
bw = max(28, (usable - gap * (n - 1)) / n * 0.55)
|
|
39
|
+
step = usable / n
|
|
40
|
+
parts = [f'<line x1="46" y1="{base}" x2="540" y2="{base}" class="s-bds" stroke-width="1"/>']
|
|
41
|
+
for i, (lb, v) in enumerate(zip(labels, values)):
|
|
42
|
+
v = float(v)
|
|
43
|
+
h = (v / max_v) * (base - top)
|
|
44
|
+
x = left + i * step + (step - bw) / 2
|
|
45
|
+
y = base - h
|
|
46
|
+
cls = 'f-acc' if i >= n - 2 else 'f-s3'
|
|
47
|
+
cx = x + bw / 2
|
|
48
|
+
parts.append(
|
|
49
|
+
f'<rect data-anim="grow" x="{x:.1f}" y="{y:.1f}" width="{bw:.1f}" '
|
|
50
|
+
f'height="{h:.1f}" rx="6" class="{cls}"><title>{esc(lb)} {v:g}{esc(unit)}</title></rect>'
|
|
51
|
+
)
|
|
52
|
+
parts.append(
|
|
53
|
+
f'<text data-anim="fade" x="{cx:.1f}" y="{base + 22}" text-anchor="middle" '
|
|
54
|
+
f'class="f-txt3" font-size="11">{esc(lb)}</text>'
|
|
55
|
+
)
|
|
56
|
+
parts.append(
|
|
57
|
+
f'<text data-anim="fade" x="{cx:.1f}" y="{y - 8:.1f}" text-anchor="middle" '
|
|
58
|
+
f'class="f-txt2" font-size="12" font-weight="600">{v:g}{esc(unit)}</text>'
|
|
59
|
+
)
|
|
60
|
+
return ('<svg class="chart" data-chart="bar" viewBox="0 0 560 220">\n '
|
|
61
|
+
+ '\n '.join(parts) + '\n </svg>')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def svg_hbar(labels, values, max_v=None, unit='') -> str:
|
|
65
|
+
max_v = float(max_v or max(values) or 1)
|
|
66
|
+
n = len(labels)
|
|
67
|
+
row_h = min(36, 180 / max(n, 1))
|
|
68
|
+
y0 = 16
|
|
69
|
+
slot_x, slot_w = 130, 340
|
|
70
|
+
parts = []
|
|
71
|
+
for i, (lb, v) in enumerate(zip(labels, values)):
|
|
72
|
+
v = float(v)
|
|
73
|
+
y = y0 + i * row_h
|
|
74
|
+
w = (v / max_v) * slot_w
|
|
75
|
+
parts.append(f'<text x="0" y="{y + 14:.1f}" class="f-txt2" font-size="12">{esc(lb)}</text>')
|
|
76
|
+
parts.append(
|
|
77
|
+
f'<rect x="{slot_x}" y="{y + 4:.1f}" width="{slot_w}" height="15" rx="7.5" class="f-s2"/>'
|
|
78
|
+
)
|
|
79
|
+
parts.append(
|
|
80
|
+
f'<rect data-anim="grow" x="{slot_x}" y="{y + 4:.1f}" width="{w:.1f}" height="15" '
|
|
81
|
+
f'rx="7.5" class="f-acc"><title>{esc(lb)} {v:g}{esc(unit)}</title></rect>'
|
|
82
|
+
)
|
|
83
|
+
parts.append(
|
|
84
|
+
f'<text x="{slot_x + slot_w + 12}" y="{y + 16:.1f}" class="f-txt" '
|
|
85
|
+
f'font-size="12" font-weight="600">{v:g}{esc(unit)}</text>'
|
|
86
|
+
)
|
|
87
|
+
return ('<svg class="chart" data-chart="hbar" viewBox="0 0 560 220">\n '
|
|
88
|
+
+ '\n '.join(parts) + '\n </svg>')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def svg_line(labels, values, max_v=None, unit='', area=False) -> str:
|
|
92
|
+
max_v = float(max_v or max(values) * 1.2 or 1)
|
|
93
|
+
n = len(labels)
|
|
94
|
+
left, right, base, top = 60, 520, 180, 40
|
|
95
|
+
xs = [left + i * (right - left) / max(n - 1, 1) for i in range(n)]
|
|
96
|
+
ys = [base - (float(v) / max_v) * (base - top) for v in values]
|
|
97
|
+
pts = ' '.join(f'{x:.1f},{y:.1f}' for x, y in zip(xs, ys))
|
|
98
|
+
path = 'L'.join(f'{x:.1f},{y:.1f}' for x, y in zip(xs, ys))
|
|
99
|
+
path = 'M' + path[1:] if path.startswith('L') else 'M' + path
|
|
100
|
+
# fix: build properly
|
|
101
|
+
path = 'M' + ' L'.join(f'{x:.1f},{y:.1f}' for x, y in zip(xs, ys))
|
|
102
|
+
ct = 'area' if area else 'line'
|
|
103
|
+
parts = [
|
|
104
|
+
f'<line x1="46" y1="{base}" x2="540" y2="{base}" class="s-bds" stroke-width="1"/>',
|
|
105
|
+
f'<line x1="46" y1="130" x2="540" y2="130" class="s-bds" stroke-width="1" stroke-dasharray="3 5"/>',
|
|
106
|
+
f'<line x1="46" y1="80" x2="540" y2="80" class="s-bds" stroke-width="1" stroke-dasharray="3 5"/>',
|
|
107
|
+
]
|
|
108
|
+
if area:
|
|
109
|
+
area_path = path + f' L{xs[-1]:.1f},{base} L{xs[0]:.1f},{base} Z'
|
|
110
|
+
parts.append(f'<path d="{area_path}" class="f-acc" fill-opacity=".12"/>')
|
|
111
|
+
parts.append(
|
|
112
|
+
f'<path data-draw d="{path}" class="f-none s-acc" stroke-width="2.5" '
|
|
113
|
+
f'stroke-linecap="round" stroke-linejoin="round"/>'
|
|
114
|
+
)
|
|
115
|
+
for i, (x, y, lb, v) in enumerate(zip(xs, ys, labels, values)):
|
|
116
|
+
parts.append(
|
|
117
|
+
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="4" class="f-acc">'
|
|
118
|
+
f'<title>{esc(lb)} · {float(v):g}{esc(unit)}</title></circle>'
|
|
119
|
+
)
|
|
120
|
+
parts.append(
|
|
121
|
+
f'<text x="{x:.1f}" y="{base + 20}" text-anchor="middle" '
|
|
122
|
+
f'class="f-txt3" font-size="11">{esc(lb)}</text>'
|
|
123
|
+
)
|
|
124
|
+
return (f'<svg class="chart" data-chart="{ct}" viewBox="0 0 560 220">\n '
|
|
125
|
+
+ '\n '.join(parts) + '\n </svg>')
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def svg_donut(labels, values, center_label='合计', unit='') -> str:
|
|
129
|
+
total = sum(float(v) for v in values) or 1
|
|
130
|
+
# multi-segment donut: layered sweeps (simplified visual with chips)
|
|
131
|
+
# Use concentric accent arc for first + legend chips for all (presentation craft)
|
|
132
|
+
r, cx, cy, circ = 54, 70, 70, 339.3
|
|
133
|
+
parts = [
|
|
134
|
+
f'<circle cx="{cx}" cy="{cy}" r="{r}" class="f-none s-bds" stroke-width="15"/>'
|
|
135
|
+
]
|
|
136
|
+
# Draw up to 3 sweeps stacked by offset — for clarity use primary accent for largest
|
|
137
|
+
ordered = sorted(zip(labels, values), key=lambda x: -float(x[1]))
|
|
138
|
+
offset = 0.0
|
|
139
|
+
colors = ['s-acc', 's-txt2', 's-txt3', 's-bds']
|
|
140
|
+
for i, (lb, v) in enumerate(ordered):
|
|
141
|
+
p = float(v) / total
|
|
142
|
+
dash = circ * p
|
|
143
|
+
gap = circ - dash
|
|
144
|
+
rot = -90 + offset * 360
|
|
145
|
+
cls = colors[i % len(colors)]
|
|
146
|
+
parts.append(
|
|
147
|
+
f'<circle data-sweep data-circ="{circ}" data-p="{p:.4f}" cx="{cx}" cy="{cy}" r="{r}" '
|
|
148
|
+
f'class="f-none {cls}" stroke-width="15" stroke-dasharray="{dash:.1f} {gap:.1f}" '
|
|
149
|
+
f'stroke-linecap="butt" transform="rotate({rot:.1f} {cx} {cy})">'
|
|
150
|
+
f'<title>{esc(lb)} {float(v):g}{esc(unit)}</title></circle>'
|
|
151
|
+
)
|
|
152
|
+
offset += p
|
|
153
|
+
top = ordered[0]
|
|
154
|
+
pct = float(top[1]) / total * 100
|
|
155
|
+
parts.append(
|
|
156
|
+
f'<text x="{cx}" y="68" text-anchor="middle" class="f-txt" font-size="22" '
|
|
157
|
+
f'font-weight="600" data-count="{pct:.0f}" data-suffix="%">{pct:.0f}%</text>'
|
|
158
|
+
)
|
|
159
|
+
parts.append(
|
|
160
|
+
f'<text x="{cx}" y="88" text-anchor="middle" class="f-txt3" font-size="11">'
|
|
161
|
+
f'{esc(center_label)}</text>'
|
|
162
|
+
)
|
|
163
|
+
# viewBox larger with legend beside — keep 560x220 and center the donut
|
|
164
|
+
inner = '\n '.join(parts)
|
|
165
|
+
return (
|
|
166
|
+
f'<svg class="chart" data-chart="donut" viewBox="0 0 560 220">\n'
|
|
167
|
+
f' <g transform="translate(210,40)">\n {inner}\n </g>\n'
|
|
168
|
+
f' </svg>'
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def svg_waterfall(labels, values, unit='') -> str:
|
|
173
|
+
# values: start, deltas..., end (end should equal start+sum(deltas))
|
|
174
|
+
n = len(labels)
|
|
175
|
+
if n < 2:
|
|
176
|
+
return svg_bar(labels, values, unit=unit)
|
|
177
|
+
start = float(values[0])
|
|
178
|
+
end = float(values[-1])
|
|
179
|
+
mids = [float(v) for v in values[1:-1]]
|
|
180
|
+
# running level after each step
|
|
181
|
+
levels = [start]
|
|
182
|
+
cur = start
|
|
183
|
+
for d in mids:
|
|
184
|
+
cur += d
|
|
185
|
+
levels.append(cur)
|
|
186
|
+
levels.append(end)
|
|
187
|
+
max_v = max(levels) * 1.15 or 1
|
|
188
|
+
min_v = min(0, min(levels))
|
|
189
|
+
span = max_v - min_v or 1
|
|
190
|
+
left, right, base, top = 50, 540, 190, 30
|
|
191
|
+
usable = right - left
|
|
192
|
+
bw = min(70, usable / n * 0.55)
|
|
193
|
+
step = usable / n
|
|
194
|
+
|
|
195
|
+
def y_of(val):
|
|
196
|
+
return base - ((val - min_v) / span) * (base - top)
|
|
197
|
+
|
|
198
|
+
parts = [f'<line x1="40" y1="{base}" x2="540" y2="{base}" class="s-bds" stroke-width="1"/>']
|
|
199
|
+
# start bar
|
|
200
|
+
y1, y0 = y_of(start), base
|
|
201
|
+
h = abs(y0 - y1)
|
|
202
|
+
x = left + (step - bw) / 2
|
|
203
|
+
parts.append(
|
|
204
|
+
f'<rect data-anim="grow" x="{x:.1f}" y="{min(y0,y1):.1f}" width="{bw:.1f}" '
|
|
205
|
+
f'height="{h:.1f}" rx="5" class="f-s3"/>'
|
|
206
|
+
)
|
|
207
|
+
parts.append(
|
|
208
|
+
f'<text x="{x + bw/2:.1f}" y="{min(y0,y1) - 8:.1f}" text-anchor="middle" '
|
|
209
|
+
f'class="f-txt2" font-size="12" font-weight="600">{start:g}{esc(unit)}</text>'
|
|
210
|
+
)
|
|
211
|
+
parts.append(
|
|
212
|
+
f'<text x="{x + bw/2:.1f}" y="{base + 16}" text-anchor="middle" '
|
|
213
|
+
f'class="f-txt3" font-size="11">{esc(labels[0])}</text>'
|
|
214
|
+
)
|
|
215
|
+
prev = start
|
|
216
|
+
for i, d in enumerate(mids):
|
|
217
|
+
idx = i + 1
|
|
218
|
+
nxt = prev + d
|
|
219
|
+
ya, yb = y_of(prev), y_of(nxt)
|
|
220
|
+
x = left + idx * step + (step - bw) / 2
|
|
221
|
+
top_y, bot_y = min(ya, yb), max(ya, yb)
|
|
222
|
+
cls = 'f-acc' if d >= 0 else 'f-txt3'
|
|
223
|
+
sign = f'+{d:g}' if d >= 0 else f'{d:g}'
|
|
224
|
+
parts.append(
|
|
225
|
+
f'<rect data-anim="grow" x="{x:.1f}" y="{top_y:.1f}" width="{bw:.1f}" '
|
|
226
|
+
f'height="{max(bot_y - top_y, 2):.1f}" rx="5" class="{cls}"/>'
|
|
227
|
+
)
|
|
228
|
+
parts.append(
|
|
229
|
+
f'<text x="{x + bw/2:.1f}" y="{top_y - 8:.1f}" text-anchor="middle" '
|
|
230
|
+
f'class="{cls}" font-size="12" font-weight="600">{sign}{esc(unit)}</text>'
|
|
231
|
+
)
|
|
232
|
+
parts.append(
|
|
233
|
+
f'<text x="{x + bw/2:.1f}" y="{base + 16}" text-anchor="middle" '
|
|
234
|
+
f'class="f-txt3" font-size="11">{esc(labels[idx])}</text>'
|
|
235
|
+
)
|
|
236
|
+
# connector
|
|
237
|
+
if i < len(mids):
|
|
238
|
+
parts.append(
|
|
239
|
+
f'<line x1="{x - (step - bw)/2:.1f}" y1="{ya:.1f}" '
|
|
240
|
+
f'x2="{x:.1f}" y2="{ya:.1f}" class="s-bds" stroke-width="1" stroke-dasharray="3 3"/>'
|
|
241
|
+
)
|
|
242
|
+
prev = nxt
|
|
243
|
+
# end bar
|
|
244
|
+
x = left + (n - 1) * step + (step - bw) / 2
|
|
245
|
+
ye = y_of(end)
|
|
246
|
+
parts.append(
|
|
247
|
+
f'<rect data-anim="grow" x="{x:.1f}" y="{ye:.1f}" width="{bw:.1f}" '
|
|
248
|
+
f'height="{base - ye:.1f}" rx="5" class="f-acc"/>'
|
|
249
|
+
)
|
|
250
|
+
parts.append(
|
|
251
|
+
f'<text x="{x + bw/2:.1f}" y="{ye - 8:.1f}" text-anchor="middle" '
|
|
252
|
+
f'class="f-txt2" font-size="12" font-weight="600">{end:g}{esc(unit)}</text>'
|
|
253
|
+
)
|
|
254
|
+
parts.append(
|
|
255
|
+
f'<text x="{x + bw/2:.1f}" y="{base + 16}" text-anchor="middle" '
|
|
256
|
+
f'class="f-txt3" font-size="11">{esc(labels[-1])}</text>'
|
|
257
|
+
)
|
|
258
|
+
return ('<svg class="chart" data-chart="waterfall" viewBox="0 0 560 220">\n '
|
|
259
|
+
+ '\n '.join(parts) + '\n </svg>')
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def build_svg(chart: dict) -> str:
|
|
263
|
+
ct = (chart.get('type') or 'bar').lower()
|
|
264
|
+
labels = chart.get('labels') or []
|
|
265
|
+
values = chart.get('values') or []
|
|
266
|
+
unit = chart.get('unit') or ''
|
|
267
|
+
max_v = chart.get('max')
|
|
268
|
+
if ct == 'hbar':
|
|
269
|
+
return svg_hbar(labels, values, max_v, unit)
|
|
270
|
+
if ct == 'line':
|
|
271
|
+
return svg_line(labels, values, max_v, unit, area=False)
|
|
272
|
+
if ct == 'area':
|
|
273
|
+
return svg_line(labels, values, max_v, unit, area=True)
|
|
274
|
+
if ct == 'donut' or ct == 'pie':
|
|
275
|
+
return svg_donut(labels, values, chart.get('centerLabel') or '合计', unit)
|
|
276
|
+
if ct == 'waterfall':
|
|
277
|
+
return svg_waterfall(labels, values, unit)
|
|
278
|
+
return svg_bar(labels, values, max_v, unit)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def iter_chart_payloads(model: dict):
|
|
282
|
+
"""Yield chart dicts in document order (sections + split sides)."""
|
|
283
|
+
for sec in model.get('sections') or []:
|
|
284
|
+
st = (sec.get('type') or '').lower()
|
|
285
|
+
if st in ('bar', 'donut', 'exhibit', 'halftable') and sec.get('chart'):
|
|
286
|
+
ch = dict(sec['chart'])
|
|
287
|
+
if st == 'donut':
|
|
288
|
+
ch.setdefault('type', 'donut')
|
|
289
|
+
elif not ch.get('type'):
|
|
290
|
+
ch['type'] = 'bar'
|
|
291
|
+
yield ch
|
|
292
|
+
for side in ('left', 'right'):
|
|
293
|
+
el = sec.get(side)
|
|
294
|
+
if isinstance(el, dict) and el.get('labels') and el.get('values') is not None:
|
|
295
|
+
ch = {
|
|
296
|
+
'type': el.get('type') or 'bar',
|
|
297
|
+
'labels': el.get('labels'),
|
|
298
|
+
'values': el.get('values'),
|
|
299
|
+
'unit': el.get('unit') or '',
|
|
300
|
+
'max': el.get('max'),
|
|
301
|
+
'centerLabel': el.get('centerLabel'),
|
|
302
|
+
}
|
|
303
|
+
yield ch
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def hydrate(html: str, model: dict) -> str:
|
|
307
|
+
payloads = list(iter_chart_payloads(model))
|
|
308
|
+
idx = 0
|
|
309
|
+
|
|
310
|
+
def repl(m):
|
|
311
|
+
nonlocal idx
|
|
312
|
+
if idx >= len(payloads):
|
|
313
|
+
return m.group(0)
|
|
314
|
+
svg = build_svg(payloads[idx])
|
|
315
|
+
idx += 1
|
|
316
|
+
return svg
|
|
317
|
+
|
|
318
|
+
new_html, n = PLACEHOLDER_SVG_RE.subn(repl, html)
|
|
319
|
+
return new_html, n, idx, len(payloads)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def main() -> int:
|
|
323
|
+
if len(sys.argv) < 2:
|
|
324
|
+
print('usage: _hydrate_charts.py <report.html>')
|
|
325
|
+
return 2
|
|
326
|
+
path = Path(sys.argv[1])
|
|
327
|
+
html = path.read_text(encoding='utf-8')
|
|
328
|
+
m = MODEL_RE.search(html)
|
|
329
|
+
if not m:
|
|
330
|
+
print('FAIL: no REPORT_MODEL')
|
|
331
|
+
return 1
|
|
332
|
+
model = json.loads(m.group(1))
|
|
333
|
+
new_html, n_ph, used, total = hydrate(html, model)
|
|
334
|
+
path.write_text(new_html, encoding='utf-8')
|
|
335
|
+
left = new_html.count('见模型数据')
|
|
336
|
+
print(f'hydrated placeholders={n_ph} used_payloads={used}/{total} remaining_placeholder={left}')
|
|
337
|
+
return 0 if left == 0 else 1
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
if __name__ == '__main__':
|
|
341
|
+
raise SystemExit(main())
|