@mindconnect-ai/mc-semantic-ui-ext-chart 0.2.1
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/chart/chart.css +25 -0
- package/dist/chart/extension.d.ts +30 -0
- package/dist/chart/extension.js +129 -0
- package/package.json +47 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Styling for charts painted by mc-semantic-ui-ext-chart.
|
|
3
|
+
*
|
|
4
|
+
* The SVG itself carries only geometry and palette colours; everything about
|
|
5
|
+
* how the chart sits on the page lives here, so an app can restyle it without
|
|
6
|
+
* touching the painter. Override --sui-chart-1 … --sui-chart-6 to recolour.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
.sui-chart { background: var(--sui-color-surface); border-radius: var(--sui-radius-lg); padding: 16px 18px; }
|
|
10
|
+
.sui-chart > h2 { font-size: 15px; font-weight: 600; margin: 0 0 10px; }
|
|
11
|
+
|
|
12
|
+
.sui-chart-svg { display: block; width: 100%; height: auto; max-width: 420px; }
|
|
13
|
+
|
|
14
|
+
/* Pie / donut: the drawing and its legend sit side by side, wrapping on narrow
|
|
15
|
+
screens rather than squeezing the chart. */
|
|
16
|
+
.sui-chart-radial { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
|
|
17
|
+
.sui-chart-radial .sui-chart-svg { width: 170px; flex: 0 0 auto; }
|
|
18
|
+
|
|
19
|
+
.sui-chart-legend { list-style: none; margin: 0; padding: 0; display: flex;
|
|
20
|
+
flex-direction: column; gap: 6px; font-size: 13px;
|
|
21
|
+
color: var(--sui-color-text-body); }
|
|
22
|
+
.sui-chart-legend li { display: flex; align-items: center; gap: 8px; }
|
|
23
|
+
.sui-chart-swatch { width: 10px; height: 10px; border-radius: 2px; flex: 0 0 auto; }
|
|
24
|
+
|
|
25
|
+
.sui-chart-empty { color: var(--sui-color-empty, #94a3b8); font-style: italic; margin: 0; }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SuiRenderer } from "@mindconnect-ai/mc-semantic-ui-core";
|
|
2
|
+
/** Wire shape of the core's `chart` node — mirrors UiChart on the Java side. */
|
|
3
|
+
interface UiChartWire {
|
|
4
|
+
type: "chart";
|
|
5
|
+
id: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
cssClass?: string;
|
|
8
|
+
chartType?: "LINE" | "BAR" | "PIE" | "DONUT" | "AREA";
|
|
9
|
+
data?: {
|
|
10
|
+
labels?: string[];
|
|
11
|
+
series?: Array<{
|
|
12
|
+
name?: string;
|
|
13
|
+
values?: number[];
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Registers the chart painter on a renderer, replacing the core's placeholder.
|
|
19
|
+
*
|
|
20
|
+
* <p>The core ships `chart` as a node type but draws nothing — it emits an
|
|
21
|
+
* empty `<div class="sui-chart" data-chart='…'>` so an addon can take over.
|
|
22
|
+
* This is that addon: one `register()` call, no charting library, plain SVG.
|
|
23
|
+
*
|
|
24
|
+
* <p>Scoped to the renderer you pass in; nothing global is patched.
|
|
25
|
+
*/
|
|
26
|
+
export declare function install(renderer: SuiRenderer): void;
|
|
27
|
+
export declare function renderChart(node: UiChartWire): string;
|
|
28
|
+
/** The drawing itself — shared entry point, mirrored by ChartPainter.java. */
|
|
29
|
+
export declare function chartSvg(node: UiChartWire): string;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// escapeHtml is inlined (not imported from the core bundle) so the compiled
|
|
2
|
+
// extension.js has NO runtime import of /sui/renderer.js. That keeps the bundle
|
|
3
|
+
// portable: it works from a CDN or under a path prefix, where an absolute
|
|
4
|
+
// /sui/ import would resolve against the wrong origin. The `import type` above
|
|
5
|
+
// is erased at compile time. Same trick as the diagram extension.
|
|
6
|
+
const HTML_ESCAPE = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
7
|
+
function esc(value) {
|
|
8
|
+
if (value == null)
|
|
9
|
+
return "";
|
|
10
|
+
return String(value).replace(/[&<>"']/g, ch => HTML_ESCAPE[ch]);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Categorical palette. Deliberately CSS variables with literal fallbacks: an
|
|
14
|
+
* app can restyle every chart from its own stylesheet, and the same markup
|
|
15
|
+
* still renders standalone (in an email, a PDF export, a CodePen).
|
|
16
|
+
*/
|
|
17
|
+
const PALETTE = [
|
|
18
|
+
"var(--sui-chart-1, #4f6bed)",
|
|
19
|
+
"var(--sui-chart-2, #29a3a3)",
|
|
20
|
+
"var(--sui-chart-3, #e0a300)",
|
|
21
|
+
"var(--sui-chart-4, #c2410c)",
|
|
22
|
+
"var(--sui-chart-5, #7c3aed)",
|
|
23
|
+
"var(--sui-chart-6, #0891b2)",
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Registers the chart painter on a renderer, replacing the core's placeholder.
|
|
27
|
+
*
|
|
28
|
+
* <p>The core ships `chart` as a node type but draws nothing — it emits an
|
|
29
|
+
* empty `<div class="sui-chart" data-chart='…'>` so an addon can take over.
|
|
30
|
+
* This is that addon: one `register()` call, no charting library, plain SVG.
|
|
31
|
+
*
|
|
32
|
+
* <p>Scoped to the renderer you pass in; nothing global is patched.
|
|
33
|
+
*/
|
|
34
|
+
export function install(renderer) {
|
|
35
|
+
renderer.register("chart", renderChart);
|
|
36
|
+
}
|
|
37
|
+
export function renderChart(node) {
|
|
38
|
+
const cls = node.cssClass ? `sui-chart ${esc(node.cssClass)}` : "sui-chart";
|
|
39
|
+
const title = node.title ? `<h2>${esc(node.title)}</h2>` : "";
|
|
40
|
+
return `<div class="${cls}" id="${esc(node.id)}" data-sui="chart">${title}${chartSvg(node)}</div>`;
|
|
41
|
+
}
|
|
42
|
+
/** The drawing itself — shared entry point, mirrored by ChartPainter.java. */
|
|
43
|
+
export function chartSvg(node) {
|
|
44
|
+
const labels = node.data?.labels ?? [];
|
|
45
|
+
const series = node.data?.series ?? [];
|
|
46
|
+
const values = series[0]?.values ?? [];
|
|
47
|
+
if (values.length === 0) {
|
|
48
|
+
return `<p class="sui-chart-empty">No data.</p>`;
|
|
49
|
+
}
|
|
50
|
+
const type = node.chartType ?? "BAR";
|
|
51
|
+
return (type === "PIE" || type === "DONUT")
|
|
52
|
+
? radialSvg(values, labels, type === "DONUT")
|
|
53
|
+
: cartesianSvg(values, labels, type);
|
|
54
|
+
}
|
|
55
|
+
// ── Cartesian: BAR / LINE / AREA ────────────────────────────────────────────
|
|
56
|
+
function cartesianSvg(values, labels, type) {
|
|
57
|
+
const W = 320, H = 170, P = 30;
|
|
58
|
+
const iw = W - 2 * P, ih = H - 2 * P;
|
|
59
|
+
const max = Math.max(1, ...values);
|
|
60
|
+
const n = values.length;
|
|
61
|
+
const axis = `<line x1="${P}" y1="${P + ih}" x2="${P + iw}" y2="${P + ih}" `
|
|
62
|
+
+ `stroke="var(--sui-color-border-strong, #94a3b8)"/>`;
|
|
63
|
+
const xLabels = labels.map((lb, i) => {
|
|
64
|
+
const x = P + (type === "BAR"
|
|
65
|
+
? (i + 0.5) * (iw / n)
|
|
66
|
+
: (n === 1 ? iw / 2 : (iw * i) / (n - 1)));
|
|
67
|
+
return `<text x="${x.toFixed(1)}" y="${H - 8}" font-size="10" text-anchor="middle" `
|
|
68
|
+
+ `fill="var(--sui-color-text-muted, #64748b)">${esc(lb)}</text>`;
|
|
69
|
+
}).join("");
|
|
70
|
+
let plot;
|
|
71
|
+
if (type === "BAR") {
|
|
72
|
+
const gap = iw / n, bw = gap * 0.6;
|
|
73
|
+
plot = values.map((v, i) => {
|
|
74
|
+
const bh = ih * (v / max);
|
|
75
|
+
const x = P + i * gap + (gap - bw) / 2;
|
|
76
|
+
const y = P + ih - bh;
|
|
77
|
+
// <title> gives every bar a native tooltip — no JS, works in SSR.
|
|
78
|
+
return `<rect x="${x.toFixed(1)}" y="${y.toFixed(1)}" width="${bw.toFixed(1)}" `
|
|
79
|
+
+ `height="${bh.toFixed(1)}" rx="2" fill="${PALETTE[0]}">`
|
|
80
|
+
+ `<title>${esc(labels[i] ?? "")}: ${v}</title></rect>`;
|
|
81
|
+
}).join("");
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const pts = values.map((v, i) => {
|
|
85
|
+
const x = P + (n === 1 ? iw / 2 : (iw * i) / (n - 1));
|
|
86
|
+
const y = P + ih - ih * (v / max);
|
|
87
|
+
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
|
88
|
+
});
|
|
89
|
+
const poly = pts.join(" ");
|
|
90
|
+
const area = type === "AREA"
|
|
91
|
+
? `<polygon points="${P},${P + ih} ${poly} ${P + iw},${P + ih}" `
|
|
92
|
+
+ `fill="var(--sui-color-primary-soft, #dbeafe)"/>`
|
|
93
|
+
: "";
|
|
94
|
+
const dots = values.map((v, i) => {
|
|
95
|
+
const [x, y] = pts[i].split(",");
|
|
96
|
+
return `<circle cx="${x}" cy="${y}" r="3" fill="${PALETTE[0]}">`
|
|
97
|
+
+ `<title>${esc(labels[i] ?? "")}: ${v}</title></circle>`;
|
|
98
|
+
}).join("");
|
|
99
|
+
plot = `${area}<polyline points="${poly}" fill="none" stroke="${PALETTE[0]}" stroke-width="2"/>${dots}`;
|
|
100
|
+
}
|
|
101
|
+
return `<svg class="sui-chart-svg" viewBox="0 0 ${W} ${H}" role="img" `
|
|
102
|
+
+ `aria-label="${esc(type.toLowerCase())} chart">${axis}${plot}${xLabels}</svg>`;
|
|
103
|
+
}
|
|
104
|
+
// ── Radial: PIE / DONUT ─────────────────────────────────────────────────────
|
|
105
|
+
function radialSvg(values, labels, donut) {
|
|
106
|
+
const cx = 85, cy = 85;
|
|
107
|
+
const r = donut ? 60 : 42;
|
|
108
|
+
// A pie is a donut whose stroke is wide enough to close the hole.
|
|
109
|
+
const sw = donut ? 20 : 84;
|
|
110
|
+
const C = 2 * Math.PI * r;
|
|
111
|
+
const total = values.reduce((a, b) => a + b, 0) || 1;
|
|
112
|
+
let offset = 0;
|
|
113
|
+
const segs = values.map((v, i) => {
|
|
114
|
+
const len = (v / total) * C;
|
|
115
|
+
const seg = `<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" `
|
|
116
|
+
+ `stroke="${PALETTE[i % PALETTE.length]}" stroke-width="${sw}" `
|
|
117
|
+
+ `stroke-dasharray="${len.toFixed(2)} ${(C - len).toFixed(2)}" `
|
|
118
|
+
+ `stroke-dashoffset="${(-offset).toFixed(2)}" transform="rotate(-90 ${cx} ${cy})">`
|
|
119
|
+
+ `<title>${esc(labels[i] ?? "")}: ${v}</title></circle>`;
|
|
120
|
+
offset += len;
|
|
121
|
+
return seg;
|
|
122
|
+
}).join("");
|
|
123
|
+
const legend = labels.map((lb, i) => `<li><span class="sui-chart-swatch" style="background:${PALETTE[i % PALETTE.length]}"></span>${esc(lb)}</li>`).join("");
|
|
124
|
+
return `<div class="sui-chart-radial">`
|
|
125
|
+
+ `<svg class="sui-chart-svg" viewBox="0 0 170 170" role="img" `
|
|
126
|
+
+ `aria-label="${donut ? "donut" : "pie"} chart">${segs}</svg>`
|
|
127
|
+
+ `<ul class="sui-chart-legend">${legend}</ul>`
|
|
128
|
+
+ `</div>`;
|
|
129
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindconnect-ai/mc-semantic-ui-ext-chart",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Chart extension for mc-semantic-ui — adds the `chart` node type and draws it as SVG",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mc-semantic-ui",
|
|
7
|
+
"server-driven-ui",
|
|
8
|
+
"chart"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/mindconnect-ai/mc-semantic-ui",
|
|
11
|
+
"bugs": "https://github.com/mindconnect-ai/mc-semantic-ui/issues",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/mindconnect-ai/mc-semantic-ui.git",
|
|
15
|
+
"directory": "ext/mc-semantic-ui-ext-chart"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"author": "David Beisert <beisdog@web.de>",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"types": "./dist/chart/extension.d.ts",
|
|
24
|
+
"main": "./dist/chart/extension.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/chart/extension.d.ts",
|
|
28
|
+
"default": "./dist/chart/extension.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json",
|
|
31
|
+
"./chart.css": "./dist/chart/chart.css"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc && node scripts/build-dist.mjs",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@mindconnect-ai/mc-semantic-ui-core": "0.2.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@mindconnect-ai/mc-semantic-ui-core": "file:../../core/mc-semantic-ui-core",
|
|
45
|
+
"typescript": "^5.4.0"
|
|
46
|
+
}
|
|
47
|
+
}
|