@antv/infographic 0.2.16 → 0.2.17
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/dist/infographic.min.js +114 -113
- package/dist/infographic.min.js.map +1 -1
- package/esm/constants/service.d.ts +1 -1
- package/esm/constants/service.js +1 -1
- package/esm/designs/structures/chart-line.js +2 -1
- package/esm/exporter/svg.js +167 -1
- package/esm/options/parser.js +8 -6
- package/esm/options/types.d.ts +3 -3
- package/esm/renderer/renderer.js +1 -1
- package/esm/resource/loaders/search.js +2 -3
- package/esm/runtime/options.js +1 -1
- package/esm/syntax/index.js +56 -10
- package/esm/syntax/mapper.js +20 -6
- package/esm/syntax/parser.js +9 -0
- package/esm/syntax/types.d.ts +1 -1
- package/esm/templates/registry.d.ts +1 -0
- package/esm/templates/registry.js +6 -0
- package/esm/templates/utils.d.ts +1 -0
- package/esm/templates/utils.js +63 -0
- package/esm/themes/built-in.js +3 -0
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/lib/constants/service.d.ts +1 -1
- package/lib/constants/service.js +1 -1
- package/lib/designs/structures/chart-line.js +2 -1
- package/lib/exporter/svg.js +167 -1
- package/lib/options/parser.js +7 -5
- package/lib/options/types.d.ts +3 -3
- package/lib/renderer/renderer.js +1 -1
- package/lib/resource/loaders/search.js +2 -3
- package/lib/runtime/options.js +1 -1
- package/lib/syntax/index.js +56 -10
- package/lib/syntax/mapper.js +20 -6
- package/lib/syntax/parser.js +9 -0
- package/lib/syntax/types.d.ts +1 -1
- package/lib/templates/registry.d.ts +1 -0
- package/lib/templates/registry.js +7 -0
- package/lib/templates/utils.d.ts +1 -0
- package/lib/templates/utils.js +66 -0
- package/lib/themes/built-in.js +3 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/constants/service.ts +1 -1
- package/src/designs/structures/chart-line.tsx +3 -1
- package/src/exporter/svg.ts +195 -1
- package/src/options/parser.ts +7 -6
- package/src/options/types.ts +3 -3
- package/src/renderer/renderer.ts +1 -1
- package/src/resource/loaders/search.ts +2 -2
- package/src/runtime/options.ts +1 -1
- package/src/syntax/index.ts +71 -10
- package/src/syntax/mapper.ts +20 -6
- package/src/syntax/parser.ts +10 -0
- package/src/syntax/types.ts +1 -0
- package/src/templates/registry.ts +6 -0
- package/src/templates/utils.ts +87 -0
- package/src/themes/built-in.ts +4 -0
- package/src/version.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ICON_SERVICE_URL = "https://
|
|
1
|
+
export declare const ICON_SERVICE_URL = "https://lab.weavefox.cn/api/v1/infographic/icon";
|
package/esm/constants/service.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ICON_SERVICE_URL = 'https://
|
|
1
|
+
export const ICON_SERVICE_URL = 'https://lab.weavefox.cn/api/v1/infographic/icon';
|
|
@@ -67,10 +67,11 @@ export const ChartLine = (props) => {
|
|
|
67
67
|
const titleElements = [];
|
|
68
68
|
const tickElements = [];
|
|
69
69
|
const ticksY = scaleY.ticks(6);
|
|
70
|
+
const formatTickY = scaleY.tickFormat(6);
|
|
70
71
|
ticksY.forEach((tick) => {
|
|
71
72
|
const yPos = chartOriginY + scaleY(tick);
|
|
72
73
|
gridElements.push(_jsx(Path, { d: `M ${chartOriginX} ${yPos} L ${chartOriginX + derivedChartWidth} ${yPos}`, width: derivedChartWidth, height: 1, stroke: axisColor, strokeWidth: 1, "data-element-type": "shape", opacity: 0.08 }));
|
|
73
|
-
tickElements.push(_jsx(Text, { x: chartOriginX - 8, y: yPos, alignHorizontal: "right", alignVertical: "middle", fontSize: 12, fill: axisColor, children:
|
|
74
|
+
tickElements.push(_jsx(Text, { x: chartOriginX - 8, y: yPos, alignHorizontal: "right", alignVertical: "middle", fontSize: 12, fill: axisColor, children: formatTickY(tick) }));
|
|
74
75
|
});
|
|
75
76
|
const xLabels = [];
|
|
76
77
|
const pointPositions = [];
|
package/esm/exporter/svg.js
CHANGED
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
};
|
|
10
10
|
import { createElement, getElementByRole, getViewBox, setAttributes, setElementRole, traverse, } from '../utils/index.js';
|
|
11
11
|
import { embedFonts } from './font.js';
|
|
12
|
+
const VIEWBOX_CHANGE_TOLERANCE = 0.5;
|
|
12
13
|
export function exportToSVGString(svg_1) {
|
|
13
14
|
return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
|
|
14
15
|
const node = yield exportToSVG(svg, options);
|
|
@@ -16,11 +17,176 @@ export function exportToSVGString(svg_1) {
|
|
|
16
17
|
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(str);
|
|
17
18
|
});
|
|
18
19
|
}
|
|
20
|
+
function getExportViewBox(svg) {
|
|
21
|
+
if (svg.hasAttribute('viewBox'))
|
|
22
|
+
return getViewBox(svg);
|
|
23
|
+
const width = parseAbsoluteLength(svg.getAttribute('width'));
|
|
24
|
+
const height = parseAbsoluteLength(svg.getAttribute('height'));
|
|
25
|
+
if (width > 0 && height > 0) {
|
|
26
|
+
return { x: 0, y: 0, width, height };
|
|
27
|
+
}
|
|
28
|
+
const rect = svg.getBoundingClientRect();
|
|
29
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
30
|
+
return { x: 0, y: 0, width: rect.width, height: rect.height };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function parseAbsoluteLength(value) {
|
|
35
|
+
if (!value)
|
|
36
|
+
return Number.NaN;
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (!trimmed)
|
|
39
|
+
return Number.NaN;
|
|
40
|
+
if (!/^[-+]?(?:\d+\.?\d*|\.\d+)(?:px)?$/.test(trimmed))
|
|
41
|
+
return Number.NaN;
|
|
42
|
+
return Number.parseFloat(trimmed);
|
|
43
|
+
}
|
|
44
|
+
function measureSpanContentHeight(span) {
|
|
45
|
+
const prevHeight = span.style.height;
|
|
46
|
+
const prevOverflow = span.style.overflow;
|
|
47
|
+
try {
|
|
48
|
+
span.style.height = 'max-content';
|
|
49
|
+
span.style.overflow = 'hidden';
|
|
50
|
+
void span.offsetHeight; // force reflow
|
|
51
|
+
return span.scrollHeight;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
span.style.height = prevHeight;
|
|
55
|
+
span.style.overflow = prevOverflow;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function measureSpanContentWidth(span) {
|
|
59
|
+
const prevWidth = span.style.width;
|
|
60
|
+
const prevOverflow = span.style.overflow;
|
|
61
|
+
try {
|
|
62
|
+
span.style.width = 'max-content';
|
|
63
|
+
span.style.overflow = 'hidden';
|
|
64
|
+
void span.offsetWidth; // force reflow
|
|
65
|
+
return span.scrollWidth;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
span.style.width = prevWidth;
|
|
69
|
+
span.style.overflow = prevOverflow;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Returns [left, top, right, bottom] in SVG coordinates for a foreignObject,
|
|
73
|
+
// accounting for flex alignment: bottom/center-aligned content can overflow,
|
|
74
|
+
// and horizontally aligned content can overflow as well.
|
|
75
|
+
function getFOContentBoundsInSVG(fo, content, toSVGCoord) {
|
|
76
|
+
const foRect = fo.getBoundingClientRect();
|
|
77
|
+
const foTopLeft = toSVGCoord(foRect.left, foRect.top);
|
|
78
|
+
const foBottomRight = toSVGCoord(foRect.right, foRect.bottom);
|
|
79
|
+
const foLeftSVG = foTopLeft.x;
|
|
80
|
+
const foTopSVG = foTopLeft.y;
|
|
81
|
+
const foRightSVG = foBottomRight.x;
|
|
82
|
+
const foBottomSVG = foBottomRight.y;
|
|
83
|
+
const foWidthSVG = foRightSVG - foLeftSVG;
|
|
84
|
+
const foHeightSVG = foBottomSVG - foTopSVG;
|
|
85
|
+
const svgUnitsPerClientPxY = foRect.height > 0 ? foHeightSVG / foRect.height : 1;
|
|
86
|
+
const svgUnitsPerClientPxX = foRect.width > 0 ? foWidthSVG / foRect.width : 1;
|
|
87
|
+
// Measure actual content dimensions
|
|
88
|
+
const realScrollHeight = measureSpanContentHeight(content);
|
|
89
|
+
const contentHeightSVG = realScrollHeight > 0
|
|
90
|
+
? realScrollHeight * svgUnitsPerClientPxY
|
|
91
|
+
: foHeightSVG;
|
|
92
|
+
const realScrollWidth = measureSpanContentWidth(content);
|
|
93
|
+
const contentWidthSVG = realScrollWidth > 0 ? realScrollWidth * svgUnitsPerClientPxX : foWidthSVG;
|
|
94
|
+
const computedStyle = window.getComputedStyle(content);
|
|
95
|
+
const alignItems = computedStyle.alignItems;
|
|
96
|
+
const justifyContent = computedStyle.justifyContent;
|
|
97
|
+
// Calculate vertical bounds
|
|
98
|
+
let top, bottom;
|
|
99
|
+
if (alignItems === 'flex-end' || alignItems === 'end') {
|
|
100
|
+
top = foBottomSVG - contentHeightSVG;
|
|
101
|
+
bottom = foBottomSVG;
|
|
102
|
+
}
|
|
103
|
+
else if (alignItems === 'center') {
|
|
104
|
+
const overflowY = contentHeightSVG - foHeightSVG;
|
|
105
|
+
top = foTopSVG - overflowY / 2;
|
|
106
|
+
bottom = foBottomSVG + overflowY / 2;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
top = foTopSVG;
|
|
110
|
+
bottom = foTopSVG + contentHeightSVG;
|
|
111
|
+
}
|
|
112
|
+
// Calculate horizontal bounds
|
|
113
|
+
let left, right;
|
|
114
|
+
if (justifyContent === 'flex-end' ||
|
|
115
|
+
justifyContent === 'end' ||
|
|
116
|
+
justifyContent === 'right') {
|
|
117
|
+
left = foRightSVG - contentWidthSVG;
|
|
118
|
+
right = foRightSVG;
|
|
119
|
+
}
|
|
120
|
+
else if (justifyContent === 'center') {
|
|
121
|
+
const overflowX = contentWidthSVG - foWidthSVG;
|
|
122
|
+
left = foLeftSVG - overflowX / 2;
|
|
123
|
+
right = foRightSVG + overflowX / 2;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
left = foLeftSVG;
|
|
127
|
+
right = foLeftSVG + contentWidthSVG;
|
|
128
|
+
}
|
|
129
|
+
return [left, top, right, bottom];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Computes a viewBox that fully covers all foreignObject text content,
|
|
133
|
+
* accounting for overflow caused by flex alignment (bottom/center align
|
|
134
|
+
* can push content outside the foreignObject bounds).
|
|
135
|
+
*/
|
|
136
|
+
function computeFullViewBox(svg) {
|
|
137
|
+
const viewBox = getExportViewBox(svg);
|
|
138
|
+
if (!viewBox)
|
|
139
|
+
return null;
|
|
140
|
+
if (typeof svg.getScreenCTM !== 'function')
|
|
141
|
+
return null;
|
|
142
|
+
const screenCTM = svg.getScreenCTM();
|
|
143
|
+
if (!screenCTM)
|
|
144
|
+
return null;
|
|
145
|
+
const inverseCTM = screenCTM.inverse();
|
|
146
|
+
const toSVGCoord = (clientX, clientY) => {
|
|
147
|
+
const pt = svg.createSVGPoint();
|
|
148
|
+
pt.x = clientX;
|
|
149
|
+
pt.y = clientY;
|
|
150
|
+
return pt.matrixTransform(inverseCTM);
|
|
151
|
+
};
|
|
152
|
+
let minX = viewBox.x;
|
|
153
|
+
let minY = viewBox.y;
|
|
154
|
+
let maxX = viewBox.x + viewBox.width;
|
|
155
|
+
let maxY = viewBox.y + viewBox.height;
|
|
156
|
+
svg
|
|
157
|
+
.querySelectorAll('foreignObject')
|
|
158
|
+
.forEach((fo) => {
|
|
159
|
+
const content = fo.firstElementChild;
|
|
160
|
+
if (!content)
|
|
161
|
+
return;
|
|
162
|
+
const [left, top, right, bottom] = getFOContentBoundsInSVG(fo, content, toSVGCoord);
|
|
163
|
+
minX = Math.min(minX, left);
|
|
164
|
+
minY = Math.min(minY, top);
|
|
165
|
+
maxX = Math.max(maxX, right);
|
|
166
|
+
maxY = Math.max(maxY, bottom);
|
|
167
|
+
});
|
|
168
|
+
const newX = minX;
|
|
169
|
+
const newY = minY;
|
|
170
|
+
const newWidth = maxX - newX;
|
|
171
|
+
const newHeight = maxY - newY;
|
|
172
|
+
if (newWidth <= viewBox.width + VIEWBOX_CHANGE_TOLERANCE &&
|
|
173
|
+
newHeight <= viewBox.height + VIEWBOX_CHANGE_TOLERANCE &&
|
|
174
|
+
newX >= viewBox.x - VIEWBOX_CHANGE_TOLERANCE &&
|
|
175
|
+
newY >= viewBox.y - VIEWBOX_CHANGE_TOLERANCE)
|
|
176
|
+
return null;
|
|
177
|
+
return `${newX} ${newY} ${newWidth} ${newHeight}`;
|
|
178
|
+
}
|
|
19
179
|
export function exportToSVG(svg_1) {
|
|
20
180
|
return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
|
|
21
181
|
const { removeBackground = false, embedResources = true, removeIds = false, } = options;
|
|
22
182
|
const clonedSVG = svg.cloneNode(true);
|
|
23
|
-
|
|
183
|
+
if (typeof document !== 'undefined') {
|
|
184
|
+
const fullViewBox = computeFullViewBox(svg);
|
|
185
|
+
if (fullViewBox) {
|
|
186
|
+
clonedSVG.setAttribute('viewBox', fullViewBox);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const { width, height } = getViewBox(clonedSVG);
|
|
24
190
|
setAttributes(clonedSVG, { width, height });
|
|
25
191
|
if (removeIds) {
|
|
26
192
|
inlineUseElements(clonedSVG);
|
package/esm/options/parser.js
CHANGED
|
@@ -10,17 +10,19 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
12
|
import { merge } from 'lodash-es';
|
|
13
|
-
import { getItem, getStructure,
|
|
13
|
+
import { getItem, getStructure, Title, } from '../designs/index.js';
|
|
14
14
|
import { getPaletteColor } from '../renderer/index.js';
|
|
15
|
+
import { getTemplate, resolveTemplateKey } from '../templates/registry.js';
|
|
15
16
|
import { generateThemeColors, getTheme } from '../themes/index.js';
|
|
16
17
|
import { isDarkColor, isNonNullableParsedDesignsOptions, parsePadding, } from '../utils/index.js';
|
|
17
18
|
export function parseOptions(options) {
|
|
18
19
|
const { container = '#container', padding = 0, template, design, theme, themeConfig, data } = options, restOptions = __rest(options, ["container", "padding", "template", "design", "theme", "themeConfig", "data"]);
|
|
20
|
+
const resolvedTemplate = template ? resolveTemplateKey(template) : undefined;
|
|
19
21
|
const parsedContainer = typeof container === 'string'
|
|
20
22
|
? document.querySelector(container) || document.createElement('div')
|
|
21
23
|
: container;
|
|
22
|
-
const templateOptions =
|
|
23
|
-
? getTemplate(
|
|
24
|
+
const templateOptions = resolvedTemplate
|
|
25
|
+
? getTemplate(resolvedTemplate)
|
|
24
26
|
: undefined;
|
|
25
27
|
const mergedThemeConfig = merge({}, templateOptions === null || templateOptions === void 0 ? void 0 : templateOptions.themeConfig, themeConfig);
|
|
26
28
|
const resolvedThemeConfig = theme || themeConfig || (templateOptions === null || templateOptions === void 0 ? void 0 : templateOptions.themeConfig)
|
|
@@ -35,11 +37,11 @@ export function parseOptions(options) {
|
|
|
35
37
|
Object.assign(parsed, restTemplateOptions);
|
|
36
38
|
}
|
|
37
39
|
Object.assign(parsed, restOptions);
|
|
38
|
-
const parsedData = parseData(data,
|
|
40
|
+
const parsedData = parseData(data, resolvedTemplate);
|
|
39
41
|
if (parsedData)
|
|
40
42
|
parsed.data = parsedData;
|
|
41
|
-
if (
|
|
42
|
-
parsed.template =
|
|
43
|
+
if (resolvedTemplate)
|
|
44
|
+
parsed.template = resolvedTemplate;
|
|
43
45
|
if ((templateOptions === null || templateOptions === void 0 ? void 0 : templateOptions.design) || design) {
|
|
44
46
|
const designOptions = Object.assign(Object.assign({}, (resolvedThemeConfig
|
|
45
47
|
? Object.assign(Object.assign({}, options), { themeConfig: resolvedThemeConfig }) : options)), { data: parsedData || data });
|
package/esm/options/types.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import type { ThemeConfig } from '../themes';
|
|
|
4
4
|
import type { Data, Padding, ParsedData } from '../types';
|
|
5
5
|
import type { Path } from '../utils';
|
|
6
6
|
export interface InfographicOptions {
|
|
7
|
-
/**
|
|
8
|
-
container?: string |
|
|
7
|
+
/** 容器,可以是选择器、Element 或 ShadowRoot */
|
|
8
|
+
container?: string | Element | ShadowRoot;
|
|
9
9
|
/** 宽度 */
|
|
10
10
|
width?: number | string;
|
|
11
11
|
/** 高度 */
|
|
@@ -34,7 +34,7 @@ export interface InfographicOptions {
|
|
|
34
34
|
elements?: ElementProps[];
|
|
35
35
|
}
|
|
36
36
|
export interface ParsedInfographicOptions {
|
|
37
|
-
container:
|
|
37
|
+
container: Element | ShadowRoot;
|
|
38
38
|
width?: number | string;
|
|
39
39
|
height?: number | string;
|
|
40
40
|
padding?: Padding;
|
package/esm/renderer/renderer.js
CHANGED
|
@@ -13,7 +13,6 @@ import { loadImageBase64Resource } from './image.js';
|
|
|
13
13
|
import { loadRemoteResource } from './remote.js';
|
|
14
14
|
import { loadSVGResource } from './svg.js';
|
|
15
15
|
const queryIcon = (query) => __awaiter(void 0, void 0, void 0, function* () {
|
|
16
|
-
var _a;
|
|
17
16
|
try {
|
|
18
17
|
const params = new URLSearchParams({ text: query, topK: '1' });
|
|
19
18
|
const url = `${ICON_SERVICE_URL}?${params.toString()}`;
|
|
@@ -21,9 +20,9 @@ const queryIcon = (query) => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
21
20
|
if (!response.ok)
|
|
22
21
|
return null;
|
|
23
22
|
const result = yield response.json();
|
|
24
|
-
if (!(result === null || result === void 0 ? void 0 : result.
|
|
23
|
+
if (!(result === null || result === void 0 ? void 0 : result.success) || !Array.isArray(result.data))
|
|
25
24
|
return null;
|
|
26
|
-
return result.data
|
|
25
|
+
return result.data[0] || null;
|
|
27
26
|
}
|
|
28
27
|
catch (error) {
|
|
29
28
|
console.error(`Failed to query icon for "${query}":`, error);
|
package/esm/runtime/options.js
CHANGED
package/esm/syntax/index.js
CHANGED
|
@@ -13,6 +13,15 @@ import { mapWithSchema } from './mapper.js';
|
|
|
13
13
|
import { parseSyntaxToAst } from './parser.js';
|
|
14
14
|
import { parseRelationsNode } from './relations.js';
|
|
15
15
|
import { DataSchema, DesignSchema, RootSchema, TemplateSchema, ThemeSchema, } from './schema.js';
|
|
16
|
+
const ALLOWED_ROOT_KEYS = new Set([
|
|
17
|
+
'infographic',
|
|
18
|
+
'template',
|
|
19
|
+
'design',
|
|
20
|
+
'data',
|
|
21
|
+
'theme',
|
|
22
|
+
'width',
|
|
23
|
+
'height',
|
|
24
|
+
]);
|
|
16
25
|
function normalizeItems(items) {
|
|
17
26
|
var _a;
|
|
18
27
|
const seen = new Set();
|
|
@@ -33,6 +42,13 @@ function normalizeItems(items) {
|
|
|
33
42
|
}
|
|
34
43
|
return normalized.reverse();
|
|
35
44
|
}
|
|
45
|
+
function assignMissingNodeIds(items) {
|
|
46
|
+
items.forEach((item) => {
|
|
47
|
+
if (!item.id && item.label) {
|
|
48
|
+
item.id = item.label;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
36
52
|
function resolveTemplate(node, errors) {
|
|
37
53
|
if (!node)
|
|
38
54
|
return undefined;
|
|
@@ -46,12 +62,42 @@ function resolveTemplate(node, errors) {
|
|
|
46
62
|
}
|
|
47
63
|
return undefined;
|
|
48
64
|
}
|
|
65
|
+
function inferTemplateFromBareFirstLine(entries, warnings) {
|
|
66
|
+
if ('infographic' in entries || 'template' in entries) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const [firstEntry] = Object.entries(entries);
|
|
70
|
+
if (!firstEntry)
|
|
71
|
+
return undefined;
|
|
72
|
+
const [key, node] = firstEntry;
|
|
73
|
+
if (ALLOWED_ROOT_KEYS.has(key) || node.kind !== 'object') {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (node.value !== undefined || Object.keys(node.entries).length > 0) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
warnings.push({
|
|
80
|
+
path: 'template',
|
|
81
|
+
line: node.line,
|
|
82
|
+
code: 'implicit_template',
|
|
83
|
+
message: 'Inferred template from a bare first line. Prefix it with "infographic" or "template" to make the syntax explicit.',
|
|
84
|
+
raw: key,
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
template: key,
|
|
88
|
+
inferredKey: key,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
49
91
|
export function parseSyntax(input) {
|
|
50
92
|
var _a;
|
|
51
93
|
const { ast, errors } = parseSyntaxToAst(input);
|
|
52
94
|
const warnings = [];
|
|
53
95
|
const options = {};
|
|
54
96
|
const mergedEntries = Object.assign({}, ast.entries);
|
|
97
|
+
const inferredTemplate = inferTemplateFromBareFirstLine(ast.entries, warnings);
|
|
98
|
+
if (inferredTemplate) {
|
|
99
|
+
delete mergedEntries[inferredTemplate.inferredKey];
|
|
100
|
+
}
|
|
55
101
|
const infographicNode = ast.entries.infographic;
|
|
56
102
|
let templateFromInfographic;
|
|
57
103
|
if (infographicNode && infographicNode.kind === 'object') {
|
|
@@ -62,17 +108,8 @@ export function parseSyntax(input) {
|
|
|
62
108
|
mergedEntries[key] = value;
|
|
63
109
|
});
|
|
64
110
|
}
|
|
65
|
-
const allowedRootKeys = new Set([
|
|
66
|
-
'infographic',
|
|
67
|
-
'template',
|
|
68
|
-
'design',
|
|
69
|
-
'data',
|
|
70
|
-
'theme',
|
|
71
|
-
'width',
|
|
72
|
-
'height',
|
|
73
|
-
]);
|
|
74
111
|
Object.keys(mergedEntries).forEach((key) => {
|
|
75
|
-
if (!
|
|
112
|
+
if (!ALLOWED_ROOT_KEYS.has(key)) {
|
|
76
113
|
errors.push({
|
|
77
114
|
path: key,
|
|
78
115
|
line: mergedEntries[key].line,
|
|
@@ -89,6 +126,9 @@ export function parseSyntax(input) {
|
|
|
89
126
|
if (!options.template && templateFromInfographic) {
|
|
90
127
|
options.template = templateFromInfographic;
|
|
91
128
|
}
|
|
129
|
+
if (!options.template && inferredTemplate) {
|
|
130
|
+
options.template = inferredTemplate.template;
|
|
131
|
+
}
|
|
92
132
|
const designNode = mergedEntries.design;
|
|
93
133
|
if (designNode) {
|
|
94
134
|
const design = mapWithSchema(designNode, DesignSchema, 'design', errors);
|
|
@@ -111,6 +151,12 @@ export function parseSyntax(input) {
|
|
|
111
151
|
const parsed = parseRelationsNode(relationsNode, errors, 'data.relations');
|
|
112
152
|
if (parsed.relations.length > 0 || parsed.items.length > 0) {
|
|
113
153
|
const current = ((_a = options.data) !== null && _a !== void 0 ? _a : {});
|
|
154
|
+
if (Array.isArray(current.items)) {
|
|
155
|
+
assignMissingNodeIds(current.items);
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(current.nodes)) {
|
|
158
|
+
assignMissingNodeIds(current.nodes);
|
|
159
|
+
}
|
|
114
160
|
// 优先使用已存在的数据列表 (sequences, lists, etc.)
|
|
115
161
|
const dataKeys = Object.keys(DataSchema.fields).filter((key) => key !== 'items' && key !== 'relations');
|
|
116
162
|
let hasStructuredData = false;
|
package/esm/syntax/mapper.js
CHANGED
|
@@ -5,11 +5,20 @@ function createValueNode(value, line) {
|
|
|
5
5
|
}
|
|
6
6
|
const HEX_COLOR_PATTERN = /^(#[0-9a-f]{8}|#[0-9a-f]{6}|#[0-9a-f]{4}|#[0-9a-f]{3})/i;
|
|
7
7
|
const FUNCTION_COLOR_PATTERN = /^((?:rgb|rgba|hsl|hsla)\([^)]*\))/i;
|
|
8
|
+
function normalizeBooleanLiteral(value) {
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (/^true$/i.test(trimmed))
|
|
11
|
+
return 'true';
|
|
12
|
+
if (/^false$/i.test(trimmed))
|
|
13
|
+
return 'false';
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
8
16
|
function parseScalar(value) {
|
|
9
17
|
const trimmed = value.trim();
|
|
10
|
-
|
|
18
|
+
const normalizedBoolean = normalizeBooleanLiteral(trimmed);
|
|
19
|
+
if (normalizedBoolean === 'true')
|
|
11
20
|
return true;
|
|
12
|
-
if (
|
|
21
|
+
if (normalizedBoolean === 'false')
|
|
13
22
|
return false;
|
|
14
23
|
if (/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
15
24
|
return parseFloat(trimmed);
|
|
@@ -175,11 +184,12 @@ export function mapWithSchema(node, schema, path, errors) {
|
|
|
175
184
|
addError(errors, node, path, 'schema_mismatch', 'Expected boolean value.');
|
|
176
185
|
return undefined;
|
|
177
186
|
}
|
|
178
|
-
|
|
187
|
+
const normalizedBoolean = normalizeBooleanLiteral(value);
|
|
188
|
+
if (!normalizedBoolean) {
|
|
179
189
|
addError(errors, node, path, 'invalid_value', 'Invalid boolean value.', value);
|
|
180
190
|
return undefined;
|
|
181
191
|
}
|
|
182
|
-
return
|
|
192
|
+
return normalizedBoolean === 'true';
|
|
183
193
|
}
|
|
184
194
|
case 'enum': {
|
|
185
195
|
const value = readScalar(node);
|
|
@@ -187,11 +197,15 @@ export function mapWithSchema(node, schema, path, errors) {
|
|
|
187
197
|
addError(errors, node, path, 'schema_mismatch', 'Expected enum value.');
|
|
188
198
|
return undefined;
|
|
189
199
|
}
|
|
190
|
-
|
|
200
|
+
const normalizedBoolean = normalizeBooleanLiteral(value);
|
|
201
|
+
const enumValue = normalizedBoolean && schema.values.includes(normalizedBoolean)
|
|
202
|
+
? normalizedBoolean
|
|
203
|
+
: value;
|
|
204
|
+
if (!schema.values.includes(enumValue)) {
|
|
191
205
|
addError(errors, node, path, 'invalid_value', 'Invalid enum value.', value);
|
|
192
206
|
return undefined;
|
|
193
207
|
}
|
|
194
|
-
return
|
|
208
|
+
return enumValue;
|
|
195
209
|
}
|
|
196
210
|
case 'array': {
|
|
197
211
|
if (node.kind === 'array') {
|
package/esm/syntax/parser.js
CHANGED
|
@@ -23,6 +23,13 @@ function getIndentInfo(line) {
|
|
|
23
23
|
function stripComments(content) {
|
|
24
24
|
return content.trimEnd();
|
|
25
25
|
}
|
|
26
|
+
function isCommentLine(content) {
|
|
27
|
+
const trimmed = content.trimStart();
|
|
28
|
+
return trimmed.startsWith('#') || trimmed.startsWith('//');
|
|
29
|
+
}
|
|
30
|
+
function isCodeFenceLine(content) {
|
|
31
|
+
return /^```[\w-]*\s*$/.test(content.trim());
|
|
32
|
+
}
|
|
26
33
|
function looksLikeRelationExpression(text) {
|
|
27
34
|
return /[<>=o.x-]{2,}/.test(text);
|
|
28
35
|
}
|
|
@@ -133,6 +140,8 @@ export function parseSyntaxToAst(input) {
|
|
|
133
140
|
if (!line.trim())
|
|
134
141
|
return;
|
|
135
142
|
const { indent, content } = getIndentInfo(line);
|
|
143
|
+
if (isCommentLine(content) || isCodeFenceLine(content))
|
|
144
|
+
return;
|
|
136
145
|
const stripped = stripComments(content);
|
|
137
146
|
if (!stripped.trim())
|
|
138
147
|
return;
|
package/esm/syntax/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface ArrayNode {
|
|
|
16
16
|
line: number;
|
|
17
17
|
items: SyntaxNode[];
|
|
18
18
|
}
|
|
19
|
-
export type SyntaxErrorCode = 'unknown_key' | 'schema_mismatch' | 'invalid_value' | 'bad_indent' | 'bad_list' | 'bad_syntax';
|
|
19
|
+
export type SyntaxErrorCode = 'implicit_template' | 'unknown_key' | 'schema_mismatch' | 'invalid_value' | 'bad_indent' | 'bad_list' | 'bad_syntax';
|
|
20
20
|
export interface SyntaxError {
|
|
21
21
|
path: string;
|
|
22
22
|
line: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TemplateOptions } from './types';
|
|
2
2
|
export declare function registerTemplate(type: string, template: TemplateOptions): void;
|
|
3
|
+
export declare function resolveTemplateKey(type: string): string | undefined;
|
|
3
4
|
export declare function getTemplate(type: string): TemplateOptions | undefined;
|
|
4
5
|
export declare function getTemplates(): string[];
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { findClosestTemplateKey } from './utils.js';
|
|
1
2
|
const TEMPLATE_REGISTRY = new Map();
|
|
2
3
|
export function registerTemplate(type, template) {
|
|
3
4
|
TEMPLATE_REGISTRY.set(type, template);
|
|
4
5
|
}
|
|
6
|
+
export function resolveTemplateKey(type) {
|
|
7
|
+
if (TEMPLATE_REGISTRY.has(type))
|
|
8
|
+
return type;
|
|
9
|
+
return findClosestTemplateKey(type, TEMPLATE_REGISTRY.keys());
|
|
10
|
+
}
|
|
5
11
|
export function getTemplate(type) {
|
|
6
12
|
return TEMPLATE_REGISTRY.get(type);
|
|
7
13
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findClosestTemplateKey(type: string, keys: Iterable<string>): string | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function normalizeTemplateKey(type) {
|
|
2
|
+
return type
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[_\s]+/g, '-')
|
|
6
|
+
.replace(/-+/g, '-');
|
|
7
|
+
}
|
|
8
|
+
function getLevenshteinDistance(source, target) {
|
|
9
|
+
if (source === target)
|
|
10
|
+
return 0;
|
|
11
|
+
if (!source.length)
|
|
12
|
+
return target.length;
|
|
13
|
+
if (!target.length)
|
|
14
|
+
return source.length;
|
|
15
|
+
const previous = Array.from({ length: target.length + 1 }, (_, index) => index);
|
|
16
|
+
const current = new Array(target.length + 1);
|
|
17
|
+
for (let sourceIndex = 1; sourceIndex <= source.length; sourceIndex += 1) {
|
|
18
|
+
current[0] = sourceIndex;
|
|
19
|
+
const sourceCode = source.charCodeAt(sourceIndex - 1);
|
|
20
|
+
for (let targetIndex = 1; targetIndex <= target.length; targetIndex += 1) {
|
|
21
|
+
const replaceCost = sourceCode === target.charCodeAt(targetIndex - 1) ? 0 : 1;
|
|
22
|
+
current[targetIndex] = Math.min(previous[targetIndex] + 1, current[targetIndex - 1] + 1, previous[targetIndex - 1] + replaceCost);
|
|
23
|
+
}
|
|
24
|
+
for (let index = 0; index < current.length; index += 1) {
|
|
25
|
+
previous[index] = current[index];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return previous[target.length];
|
|
29
|
+
}
|
|
30
|
+
function getCommonPrefixLength(source, target) {
|
|
31
|
+
const limit = Math.min(source.length, target.length);
|
|
32
|
+
let index = 0;
|
|
33
|
+
while (index < limit && source[index] === target[index]) {
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
return index;
|
|
37
|
+
}
|
|
38
|
+
export function findClosestTemplateKey(type, keys) {
|
|
39
|
+
const normalizedType = normalizeTemplateKey(type);
|
|
40
|
+
if (!normalizedType)
|
|
41
|
+
return undefined;
|
|
42
|
+
let bestMatch;
|
|
43
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
44
|
+
let bestPrefixLength = -1;
|
|
45
|
+
for (const key of keys) {
|
|
46
|
+
const normalizedKey = normalizeTemplateKey(key);
|
|
47
|
+
if (normalizedKey === normalizedType) {
|
|
48
|
+
return key;
|
|
49
|
+
}
|
|
50
|
+
const distance = getLevenshteinDistance(normalizedType, normalizedKey);
|
|
51
|
+
const prefixLength = getCommonPrefixLength(normalizedType, normalizedKey);
|
|
52
|
+
if (distance < bestDistance ||
|
|
53
|
+
(distance === bestDistance && prefixLength > bestPrefixLength) ||
|
|
54
|
+
(distance === bestDistance &&
|
|
55
|
+
prefixLength === bestPrefixLength &&
|
|
56
|
+
(!bestMatch || key < bestMatch))) {
|
|
57
|
+
bestMatch = key;
|
|
58
|
+
bestDistance = distance;
|
|
59
|
+
bestPrefixLength = prefixLength;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return bestMatch;
|
|
63
|
+
}
|
package/esm/themes/built-in.js
CHANGED
package/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.17";
|
package/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
1
|
+
export const VERSION = '0.2.17';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const ICON_SERVICE_URL = "https://
|
|
1
|
+
export declare const ICON_SERVICE_URL = "https://lab.weavefox.cn/api/v1/infographic/icon";
|
package/lib/constants/service.js
CHANGED
|
@@ -70,10 +70,11 @@ const ChartLine = (props) => {
|
|
|
70
70
|
const titleElements = [];
|
|
71
71
|
const tickElements = [];
|
|
72
72
|
const ticksY = scaleY.ticks(6);
|
|
73
|
+
const formatTickY = scaleY.tickFormat(6);
|
|
73
74
|
ticksY.forEach((tick) => {
|
|
74
75
|
const yPos = chartOriginY + scaleY(tick);
|
|
75
76
|
gridElements.push((0, jsx_runtime_1.jsx)(jsx_1.Path, { d: `M ${chartOriginX} ${yPos} L ${chartOriginX + derivedChartWidth} ${yPos}`, width: derivedChartWidth, height: 1, stroke: axisColor, strokeWidth: 1, "data-element-type": "shape", opacity: 0.08 }));
|
|
76
|
-
tickElements.push((0, jsx_runtime_1.jsx)(jsx_1.Text, { x: chartOriginX - 8, y: yPos, alignHorizontal: "right", alignVertical: "middle", fontSize: 12, fill: axisColor, children:
|
|
77
|
+
tickElements.push((0, jsx_runtime_1.jsx)(jsx_1.Text, { x: chartOriginX - 8, y: yPos, alignHorizontal: "right", alignVertical: "middle", fontSize: 12, fill: axisColor, children: formatTickY(tick) }));
|
|
77
78
|
});
|
|
78
79
|
const xLabels = [];
|
|
79
80
|
const pointPositions = [];
|