Skip to content

Commit 792f982

Browse files
authored
feat: add hierarchy structure (#133)
* feat(designs): add hierarchy-strcture * refactor(dev): update dev env * refactor: update template list * chore: optimize infographic template updater skill * fix: fix cr issue * refactor(designs): hierarchy-structures support layerLabelPosition * refactor: reuse data between dev and site gallery * fix: fix cr issues * chore: update version to 0.2.4
1 parent 7ab5cf9 commit 792f982

20 files changed

Lines changed: 973 additions & 578 deletions

File tree

.skills/infographic-creator/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ interface ItemDatum {
177177
- hierarchy-tree-tech-style-capsule-item
178178
- hierarchy-tree-curved-line-rounded-rect-node
179179
- hierarchy-tree-tech-style-badge-card
180+
- hierarchy-structure
180181
- chart-column-simple
181182
- chart-bar-plain-text
182183
- chart-line-plain-text

.skills/infographic-template-updater/SKILL.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,15 @@ Update public template lists and gallery mappings when new templates are added i
1212
## Workflow
1313

1414
1. Collect new template names from the added `src/templates/*.ts` file (object keys).
15+
- If templates are composed via spreads (e.g. `...listZigzagTemplates`), also confirm the final keys in `src/templates/built-in.ts`.
1516
2. Update template lists:
1617
- `SKILL.md` in the "Available Templates" list.
1718
- `site/src/components/AIPlayground/Prompt.ts` in the template list.
1819
Keep existing ordering/grouping; add new `list-*` entries near other list templates.
19-
3. Update gallery mapping in `site/src/components/Gallery/templates.ts`:
20-
- Add entries to `TEMPLATE_ENTRIES` with the correct dataset (most list templates use `DATASET.LIST`).
21-
- If the template should be featured, add it to `PREMIUM_TEMPLATE_KEYS` in the desired order.
22-
4. Sanity check with `rg -n "<template-name>"` across the three files to confirm presence.
20+
3. Sanity check with `rg -n "<template-name>"` across the three files to confirm presence.
2321

2422
## Notes
2523

2624
- Do not remove or rename existing entries.
2725
- Keep template names exact and lower-case.
28-
- If a template fits a different dataset, follow the dataset used by similar templates.
26+
- If a template needs example data, update or extend `site/src/components/Gallery/datasets.ts` to match its structure.

dev/src/Composite.tsx

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
Data,
3-
getItems,
4-
getStructures,
5-
InfographicOptions,
6-
} from '@antv/infographic';
1+
import { getItems, getStructures, InfographicOptions } from '@antv/infographic';
72
import Editor from '@monaco-editor/react';
83
import {
94
Button,
@@ -17,23 +12,9 @@ import {
1712
} from 'antd';
1813
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1914
import { Infographic } from './Infographic';
20-
import {
21-
COMPARE_DATA,
22-
HIERARCHY_DATA,
23-
LIST_DATA,
24-
SWOT_DATA,
25-
WORD_CLOUD_DATA,
26-
} from './data';
15+
import { DATA_KEYS, DATASET, DEFAULT_DATA_KEY, type DataKey } from './data';
2716
import { getStoredValues, setStoredValues } from './utils/storage';
2817

29-
const DATA: { label: string; key: string; value: Data }[] = [
30-
{ label: '列表数据', key: 'list', value: LIST_DATA },
31-
{ label: '层级数据', key: 'hierarchy', value: HIERARCHY_DATA },
32-
{ label: '对比数据', key: 'compare', value: COMPARE_DATA },
33-
{ label: 'SWOT数据', key: 'swot', value: SWOT_DATA },
34-
{ label: '词云数据', key: 'wordcloud', value: WORD_CLOUD_DATA },
35-
];
36-
3718
const items = getItems();
3819
const structures = getStructures();
3920

@@ -43,7 +24,7 @@ export const Composite = () => {
4324
const defaultValues = {
4425
structure: structures[0] || 'list-grid',
4526
item: items[0] || 'circular-progress',
46-
data: 'list',
27+
data: DEFAULT_DATA_KEY,
4728
theme: 'light' as const,
4829
colorPrimary: '#FF356A',
4930
enablePrimary: true,
@@ -68,7 +49,7 @@ export const Composite = () => {
6849
}
6950

7051
// Validate data
71-
const dataKeys = DATA.map((d) => d.key);
52+
const dataKeys = DATA_KEYS;
7253
if (stored.data && !dataKeys.includes(stored.data)) {
7354
fallbacks.data = dataKeys[0];
7455
}
@@ -85,7 +66,7 @@ export const Composite = () => {
8566
structure: string;
8667
item: string;
8768
item2: string;
88-
data: string;
69+
data: DataKey;
8970
theme: 'light' | 'dark';
9071
colorPrimary: string;
9172
enablePrimary: boolean;
@@ -216,7 +197,7 @@ export const Composite = () => {
216197
structure: structureObj,
217198
items: item2Obj ? [itemObj, item2Obj] : [itemObj],
218199
},
219-
data: DATA.find((it) => it.key === data)?.value,
200+
data: DATASET[data],
220201
themeConfig: {},
221202
};
222203

@@ -314,8 +295,8 @@ export const Composite = () => {
314295
</Form.Item>
315296
<Form.Item label="数据" name="data">
316297
<Select
317-
options={DATA.map(({ label, key }) => ({
318-
label,
298+
options={DATA_KEYS.map((key) => ({
299+
label: key,
319300
value: key,
320301
}))}
321302
/>

dev/src/Infographic.tsx

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,90 @@ import {
55
Infographic as Renderer,
66
} from '@antv/infographic';
77
import { useEffect, useRef } from 'react';
8-
import { getAsset } from './get-asset';
8+
9+
const svgTextCache = new Map<string, string>();
10+
const pendingRequests = new Map<string, Promise<string | null>>();
911

1012
registerResourceLoader(async (config) => {
11-
const { data } = config;
12-
const type = data.startsWith('illus:') ? 'illustration' : 'icon';
13-
const normalized = data.replace(/^illus:|^icon:/, '');
14-
const str = await getAsset(type, normalized);
15-
return loadSVGResource(str);
13+
const { data, scene } = config;
14+
15+
try {
16+
const key = `${scene}::${data}`;
17+
let svgText: string | null;
18+
19+
// 1. 命中缓存
20+
if (svgTextCache.has(key)) {
21+
svgText = svgTextCache.get(key)!;
22+
}
23+
// 2. 已有请求在进行中
24+
else if (pendingRequests.has(key)) {
25+
svgText = await pendingRequests.get(key)!;
26+
}
27+
// 3. 发起新请求
28+
else {
29+
const fetchPromise = (async () => {
30+
try {
31+
let url: string | null;
32+
33+
if (scene === 'icon') {
34+
url = `https://api.iconify.design/${data}.svg`;
35+
} else if (scene === 'illus') {
36+
url = `https://raw.githubusercontent.com/balazser/undraw-svg-collection/refs/heads/main/svgs/${data}.svg`;
37+
} else return null;
38+
39+
if (!url) return null;
40+
41+
const response = await fetch(url);
42+
43+
if (!response.ok) {
44+
console.error(`HTTP ${response.status}: Failed to load ${url}`);
45+
return null;
46+
}
47+
48+
const text = await response.text();
49+
50+
if (!text || !text.trim().startsWith('<svg')) {
51+
console.error(`Invalid SVG content from ${url}`);
52+
return null;
53+
}
54+
55+
svgTextCache.set(key, text);
56+
return text;
57+
} catch (fetchError) {
58+
console.error(`Failed to fetch resource ${key}:`, fetchError);
59+
return null;
60+
}
61+
})();
62+
63+
pendingRequests.set(key, fetchPromise);
64+
65+
try {
66+
svgText = await fetchPromise;
67+
} catch (error) {
68+
console.error(`Error loading resource ${key}:`, error);
69+
return null;
70+
} finally {
71+
pendingRequests.delete(key);
72+
}
73+
}
74+
75+
if (!svgText) {
76+
return null;
77+
}
78+
79+
const resource = loadSVGResource(svgText);
80+
81+
if (!resource) {
82+
console.error(`loadSVGResource returned null for ${key}`);
83+
svgTextCache.delete(key);
84+
return null;
85+
}
86+
87+
return resource;
88+
} catch (error) {
89+
console.error('Unexpected error in resource loader:', error);
90+
return null;
91+
}
1692
});
1793

1894
export const Infographic = ({

dev/src/Preview.tsx

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,25 @@ import { getTemplate, getTemplates, ThemeConfig } from '@antv/infographic';
22
import Editor from '@monaco-editor/react';
33
import { Button, Card, Checkbox, ColorPicker, Form, Select } from 'antd';
44
import { useEffect, useMemo, useState } from 'react';
5+
import { getDataByTemplate } from '../../shared/get-template-data';
56
import { Infographic } from './Infographic';
6-
import {
7-
COMPARE_DATA,
8-
HIERARCHY_DATA,
9-
LIST_DATA,
10-
SWOT_DATA,
11-
WORD_CLOUD_DATA,
12-
} from './data';
7+
import { DATA_KEYS, DATASET, DEFAULT_DATA_KEY, type DataKey } from './data';
138
import { getStoredValues, setStoredValues } from './utils/storage';
149

1510
const templates = getTemplates();
1611
const STORAGE_KEY = 'preview-form-values';
1712

18-
const DATA = {
19-
list: { label: '列表数据', value: LIST_DATA },
20-
hierarchy: { label: '层级数据', value: HIERARCHY_DATA },
21-
compare: { label: '对比数据', value: COMPARE_DATA },
22-
swot: { label: 'SWOT 数据', value: SWOT_DATA },
23-
wordcloud: { label: '词云数据', value: WORD_CLOUD_DATA },
24-
} as const;
25-
const TEMPLATE_DATA_MATCHERS: Array<[string, keyof typeof DATA]> = [
26-
['hierarchy-', 'hierarchy'],
27-
['compare-', 'compare'],
28-
['swot-', 'swot'],
29-
['chart-wordcloud', 'wordcloud'],
30-
];
31-
const getDefaultDataString = (key: keyof typeof DATA) =>
32-
JSON.stringify(DATA[key].value, null, 2);
33-
const getDataByTemplate = (nextTemplate: string): keyof typeof DATA => {
34-
for (const [prefix, dataKey] of TEMPLATE_DATA_MATCHERS) {
35-
if (nextTemplate.startsWith(prefix)) {
36-
return dataKey;
37-
}
38-
}
39-
return 'list';
40-
};
13+
const getDefaultDataString = (key: DataKey) =>
14+
JSON.stringify(DATASET[key], null, 2);
15+
16+
const resolvePreviewDataKey = (data: unknown) =>
17+
DATA_KEYS.find((key) => DATASET[key] === data) ?? DEFAULT_DATA_KEY;
4118

4219
export const Preview = () => {
4320
// Get stored values with validation
4421
const storedValues = getStoredValues<{
4522
template: string;
46-
data: keyof typeof DATA;
23+
data: DataKey;
4724
theme: 'light' | 'dark' | 'hand-drawn';
4825
colorPrimary: string;
4926
enablePrimary: boolean;
@@ -57,7 +34,7 @@ export const Preview = () => {
5734
}
5835

5936
// Validate data
60-
const dataKeys = Object.keys(DATA) as (keyof typeof DATA)[];
37+
const dataKeys = DATA_KEYS;
6138
if (stored.data && !dataKeys.includes(stored.data)) {
6239
fallbacks.data = dataKeys[0];
6340
}
@@ -66,20 +43,20 @@ export const Preview = () => {
6643
});
6744

6845
const initialTemplate = storedValues?.template || templates[0];
69-
const initialData = storedValues?.data || 'list';
46+
const initialData = storedValues?.data || DEFAULT_DATA_KEY;
7047
const initialTheme = storedValues?.theme || 'light';
7148
const initialColorPrimary = storedValues?.colorPrimary || '#FF356A';
7249
const initialEnablePrimary = storedValues?.enablePrimary ?? true;
7350
const initialEnablePalette = storedValues?.enablePalette || false;
7451

7552
const [template, setTemplate] = useState(initialTemplate);
76-
const [data, setData] = useState<keyof typeof DATA>(initialData);
53+
const [data, setData] = useState<DataKey>(initialData);
7754
const [theme, setTheme] = useState<string>(initialTheme);
7855
const [colorPrimary, setColorPrimary] = useState(initialColorPrimary);
7956
const [enablePrimary, setEnablePrimary] = useState(initialEnablePrimary);
8057
const [enablePalette, setEnablePalette] = useState(initialEnablePalette);
8158
const [customData, setCustomData] = useState<string>(() =>
82-
JSON.stringify(DATA[initialData].value, null, 2),
59+
JSON.stringify(DATASET[initialData], null, 2),
8360
);
8461
const [dataError, setDataError] = useState<string>('');
8562

@@ -125,10 +102,14 @@ export const Preview = () => {
125102

126103
const applyTemplate = (nextTemplate: string) => {
127104
const nextData = getDataByTemplate(nextTemplate);
105+
const nextSelection = {
106+
key: resolvePreviewDataKey(nextData),
107+
data: nextData,
108+
};
128109
setTemplate(nextTemplate);
129-
if (nextData !== data) {
130-
setData(nextData);
131-
setCustomData(getDefaultDataString(nextData));
110+
if (nextSelection.key !== data) {
111+
setData(nextSelection.key);
112+
setCustomData(JSON.stringify(nextSelection.data, null, 2));
132113
setDataError('');
133114
}
134115
};
@@ -161,7 +142,7 @@ export const Preview = () => {
161142
return parsed;
162143
} catch (error) {
163144
setDataError(error instanceof Error ? error.message : 'Invalid JSON');
164-
return DATA[data].value;
145+
return DATASET[data];
165146
}
166147
}, [customData, data]);
167148

@@ -251,8 +232,8 @@ export const Preview = () => {
251232
<Form.Item label="数据">
252233
<Select
253234
value={data}
254-
options={Object.entries(DATA).map(([key, { label }]) => ({
255-
label,
235+
options={DATA_KEYS.map((key) => ({
236+
label: key,
256237
value: key,
257238
}))}
258239
onChange={(value) => {

0 commit comments

Comments
 (0)