@memberjunction/core-actions 2.114.0 → 2.116.0
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/custom/ai/find-candidate-agents.action.js +3 -3
- package/dist/custom/ai/find-candidate-agents.action.js.map +1 -1
- package/dist/custom/ai/load-agent-spec.action.d.ts +3 -2
- package/dist/custom/ai/load-agent-spec.action.d.ts.map +1 -1
- package/dist/custom/ai/load-agent-spec.action.js +8 -7
- package/dist/custom/ai/load-agent-spec.action.js.map +1 -1
- package/dist/custom/code-execution/execute-code.action.d.ts +1 -0
- package/dist/custom/code-execution/execute-code.action.d.ts.map +1 -1
- package/dist/custom/code-execution/execute-code.action.js +6 -2
- package/dist/custom/code-execution/execute-code.action.js.map +1 -1
- package/dist/custom/visualization/create-svg-chart.action.d.ts +88 -24
- package/dist/custom/visualization/create-svg-chart.action.d.ts.map +1 -1
- package/dist/custom/visualization/create-svg-chart.action.js +779 -79
- package/dist/custom/visualization/create-svg-chart.action.js.map +1 -1
- package/dist/generated/action_subclasses.d.ts +23 -20
- package/dist/generated/action_subclasses.d.ts.map +1 -1
- package/dist/generated/action_subclasses.js +85 -85
- package/dist/generated/action_subclasses.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +17 -17
- package/readme.md +52 -0
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
2
18
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
19
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
20
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
21
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
22
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
23
|
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
25
|
+
if (mod && mod.__esModule) return mod;
|
|
26
|
+
var result = {};
|
|
27
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
28
|
+
__setModuleDefault(result, mod);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
8
31
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
32
|
exports.LoadCreateSVGChartAction = exports.CreateSVGChartAction = void 0;
|
|
10
33
|
const actions_1 = require("@memberjunction/actions");
|
|
11
34
|
const global_1 = require("@memberjunction/global");
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
const d3Scale = __importStar(require("d3-scale"));
|
|
36
|
+
const d3Shape = __importStar(require("d3-shape"));
|
|
37
|
+
const svg_utils_1 = require("./shared/svg-utils");
|
|
38
|
+
const svg_theming_1 = require("./shared/svg-theming");
|
|
15
39
|
/**
|
|
16
|
-
* Action that generates SVG charts from data using
|
|
17
|
-
* Supports
|
|
40
|
+
* Action that generates SVG charts from data using D3.
|
|
41
|
+
* Supports bar, line, pie, scatter, and area charts.
|
|
18
42
|
*
|
|
19
43
|
* This action is designed for AI agents and workflows to create publication-quality
|
|
20
44
|
* visualizations from structured data without writing visualization code.
|
|
@@ -27,29 +51,36 @@ const global_1 = require("@memberjunction/global");
|
|
|
27
51
|
* Params: [
|
|
28
52
|
* { Name: 'ChartType', Value: 'bar' },
|
|
29
53
|
* { Name: 'Data', Value: JSON.stringify([
|
|
30
|
-
* {
|
|
31
|
-
* {
|
|
32
|
-
* {
|
|
54
|
+
* { label: 'A', value: 28 },
|
|
55
|
+
* { label: 'B', value: 55 },
|
|
56
|
+
* { label: 'C', value: 43 }
|
|
33
57
|
* ]) },
|
|
34
|
-
* { Name: 'XField', Value: 'category' },
|
|
35
|
-
* { Name: 'YField', Value: 'value' },
|
|
36
58
|
* { Name: 'Title', Value: 'Sample Bar Chart' }
|
|
37
59
|
* ]
|
|
38
60
|
* });
|
|
39
61
|
*
|
|
62
|
+
* // Line chart with X/Y data
|
|
63
|
+
* await runAction({
|
|
64
|
+
* ActionName: 'Create SVG Chart',
|
|
65
|
+
* Params: [
|
|
66
|
+
* { Name: 'ChartType', Value: 'line' },
|
|
67
|
+
* { Name: 'Data', Value: JSON.stringify([
|
|
68
|
+
* { x: 1, y: 10 },
|
|
69
|
+
* { x: 2, y: 25 },
|
|
70
|
+
* { x: 3, y: 15 }
|
|
71
|
+
* ]) }
|
|
72
|
+
* ]
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
40
75
|
* // Pie chart
|
|
41
76
|
* await runAction({
|
|
42
77
|
* ActionName: 'Create SVG Chart',
|
|
43
78
|
* Params: [
|
|
44
79
|
* { Name: 'ChartType', Value: 'pie' },
|
|
45
80
|
* { Name: 'Data', Value: JSON.stringify([
|
|
46
|
-
* {
|
|
47
|
-
* {
|
|
48
|
-
* ]) }
|
|
49
|
-
* { Name: 'ThetaField', Value: 'count' },
|
|
50
|
-
* { Name: 'ColorField', Value: 'category' },
|
|
51
|
-
* { Name: 'Width', Value: '400' },
|
|
52
|
-
* { Name: 'Height', Value: '300' }
|
|
81
|
+
* { label: 'LLM', value: 67 },
|
|
82
|
+
* { label: 'Embeddings', value: 10 }
|
|
83
|
+
* ]) }
|
|
53
84
|
* ]
|
|
54
85
|
* });
|
|
55
86
|
* ```
|
|
@@ -59,16 +90,16 @@ let CreateSVGChartAction = class CreateSVGChartAction extends actions_1.BaseActi
|
|
|
59
90
|
* Generates an SVG chart from the provided data and configuration
|
|
60
91
|
*
|
|
61
92
|
* @param params - The action parameters containing:
|
|
62
|
-
* - ChartType: Type of chart (bar, line, pie, scatter, area
|
|
93
|
+
* - ChartType: Type of chart (bar, line, pie, scatter, area)
|
|
63
94
|
* - Data: JSON array of data objects
|
|
64
|
-
* - XField: Field name for X axis (for bar, line, scatter, area)
|
|
65
|
-
* - YField: Field name for Y axis (for bar, line, scatter, area)
|
|
66
|
-
* - ThetaField: Field name for pie slice size (for pie charts)
|
|
67
|
-
* - ColorField: Field name for color encoding (optional)
|
|
68
95
|
* - Title: Chart title (optional)
|
|
69
|
-
* -
|
|
70
|
-
* -
|
|
71
|
-
* -
|
|
96
|
+
* - XAxisLabel: X-axis label (optional)
|
|
97
|
+
* - YAxisLabel: Y-axis label (optional)
|
|
98
|
+
* - Width: Chart width in pixels (optional, default: 800)
|
|
99
|
+
* - Height: Chart height in pixels (optional, default: 600)
|
|
100
|
+
* - Palette: Color palette name (optional, default: 'mjDefault')
|
|
101
|
+
* - ShowGrid: Show grid lines (optional, default: false)
|
|
102
|
+
* - ShowLegend: Show legend (optional, default: false)
|
|
72
103
|
*
|
|
73
104
|
* @returns A promise resolving to an ActionResultSimple with:
|
|
74
105
|
* - Success: true if chart was generated successfully
|
|
@@ -76,60 +107,729 @@ let CreateSVGChartAction = class CreateSVGChartAction extends actions_1.BaseActi
|
|
|
76
107
|
* - Message: The SVG string or error message
|
|
77
108
|
*/
|
|
78
109
|
async InternalRunAction(params) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
110
|
+
try {
|
|
111
|
+
// Extract chart type
|
|
112
|
+
const chartTypeParam = this.getParamValue(params, 'ChartType');
|
|
113
|
+
if (!chartTypeParam) {
|
|
114
|
+
return {
|
|
115
|
+
Success: false,
|
|
116
|
+
Message: "ChartType parameter is required (bar, line, pie, scatter, area)",
|
|
117
|
+
ResultCode: "MISSING_PARAMETERS"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const chartType = this.ensureString(chartTypeParam, 'ChartType').toLowerCase();
|
|
121
|
+
const validTypes = ['bar', 'line', 'pie', 'scatter', 'area'];
|
|
122
|
+
if (!validTypes.includes(chartType)) {
|
|
123
|
+
return {
|
|
124
|
+
Success: false,
|
|
125
|
+
Message: `Invalid ChartType: ${chartType}. Valid types: ${validTypes.join(', ')}`,
|
|
126
|
+
ResultCode: "INVALID_CHART_TYPE"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Extract data
|
|
130
|
+
const dataParam = this.getParamValue(params, 'Data');
|
|
131
|
+
if (!dataParam) {
|
|
132
|
+
return {
|
|
133
|
+
Success: false,
|
|
134
|
+
Message: "Data parameter is required",
|
|
135
|
+
ResultCode: "MISSING_PARAMETERS"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const data = this.parseJSON(dataParam, 'Data');
|
|
139
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
140
|
+
return {
|
|
141
|
+
Success: false,
|
|
142
|
+
Message: "Data must be a non-empty JSON array",
|
|
143
|
+
ResultCode: "INVALID_DATA"
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// Parse common parameters
|
|
147
|
+
const width = parseInt(this.ensureString(this.getParamValue(params, 'Width') || '800', 'Width'));
|
|
148
|
+
const height = parseInt(this.ensureString(this.getParamValue(params, 'Height') || '600', 'Height'));
|
|
149
|
+
const title = this.ensureString(this.getParamValue(params, 'Title') || '', 'Title');
|
|
150
|
+
const xAxisLabel = this.ensureString(this.getParamValue(params, 'XAxisLabel') || '', 'XAxisLabel');
|
|
151
|
+
const yAxisLabel = this.ensureString(this.getParamValue(params, 'YAxisLabel') || '', 'YAxisLabel');
|
|
152
|
+
const paletteName = this.ensureString(this.getParamValue(params, 'Palette') || 'mjDefault', 'Palette');
|
|
153
|
+
const showGridParam = this.getParamValue(params, 'ShowGrid');
|
|
154
|
+
const showGrid = showGridParam ? this.ensureString(showGridParam, 'ShowGrid').toLowerCase() === 'true' : false;
|
|
155
|
+
const showLegendParam = this.getParamValue(params, 'ShowLegend');
|
|
156
|
+
const showLegend = showLegendParam ? this.ensureString(showLegendParam, 'ShowLegend').toLowerCase() === 'true' : false;
|
|
157
|
+
// Create branding configuration
|
|
158
|
+
const branding = {
|
|
159
|
+
palette: { type: 'named', name: paletteName }
|
|
160
|
+
};
|
|
161
|
+
// Create viewBox configuration
|
|
162
|
+
const viewBox = {
|
|
163
|
+
width,
|
|
164
|
+
height,
|
|
165
|
+
padding: { top: title ? 60 : 40, right: 40, bottom: xAxisLabel ? 80 : 60, left: yAxisLabel ? 80 : 60 }
|
|
166
|
+
};
|
|
167
|
+
// Generate chart based on type
|
|
168
|
+
let svg;
|
|
169
|
+
switch (chartType) {
|
|
170
|
+
case 'bar':
|
|
171
|
+
svg = await this.renderBarChart(data, viewBox, branding, title, xAxisLabel, yAxisLabel, showGrid, showLegend);
|
|
172
|
+
break;
|
|
173
|
+
case 'line':
|
|
174
|
+
svg = await this.renderLineChart(data, viewBox, branding, title, xAxisLabel, yAxisLabel, showGrid, showLegend);
|
|
175
|
+
break;
|
|
176
|
+
case 'area':
|
|
177
|
+
svg = await this.renderAreaChart(data, viewBox, branding, title, xAxisLabel, yAxisLabel, showGrid, showLegend);
|
|
178
|
+
break;
|
|
179
|
+
case 'pie':
|
|
180
|
+
svg = await this.renderPieChart(data, viewBox, branding, title, showLegend);
|
|
181
|
+
break;
|
|
182
|
+
case 'scatter':
|
|
183
|
+
svg = await this.renderScatterChart(data, viewBox, branding, title, xAxisLabel, yAxisLabel, showGrid, showLegend);
|
|
184
|
+
break;
|
|
185
|
+
default:
|
|
186
|
+
return {
|
|
187
|
+
Success: false,
|
|
188
|
+
Message: `Unsupported chart type: ${chartType}`,
|
|
189
|
+
ResultCode: "INVALID_CHART_TYPE"
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
Success: true,
|
|
194
|
+
ResultCode: "SUCCESS",
|
|
195
|
+
Message: svg,
|
|
196
|
+
svg,
|
|
197
|
+
width,
|
|
198
|
+
height
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
return {
|
|
203
|
+
Success: false,
|
|
204
|
+
Message: `Failed to generate chart: ${error instanceof Error ? error.message : String(error)}`,
|
|
205
|
+
ResultCode: "CHART_GENERATION_FAILED"
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Renders a bar chart
|
|
211
|
+
*/
|
|
212
|
+
async renderBarChart(data, viewBox, branding, title, xAxisLabel, yAxisLabel, showGrid, _showLegend) {
|
|
213
|
+
const vb = svg_utils_1.SVGUtils.calculateViewBox(viewBox);
|
|
214
|
+
const doc = svg_utils_1.SVGUtils.createSVG(viewBox.width, viewBox.height, 'bar-chart');
|
|
215
|
+
const svg = doc.querySelector('svg');
|
|
216
|
+
const ns = svg.namespaceURI;
|
|
217
|
+
// Add accessibility and styles
|
|
218
|
+
if (title) {
|
|
219
|
+
svg_utils_1.SVGUtils.addA11y(svg, { title, ariaRole: 'img' });
|
|
220
|
+
}
|
|
221
|
+
svg_utils_1.SVGUtils.addStyles(svg, (0, svg_theming_1.generateCSS)(branding));
|
|
222
|
+
const palette = (0, svg_theming_1.getPalette)(branding.palette);
|
|
223
|
+
const font = (0, svg_theming_1.getFontSpec)(branding.font);
|
|
224
|
+
// Extract labels and values
|
|
225
|
+
const labels = data.map(d => d.label || String(d.x || ''));
|
|
226
|
+
const values = data.map(d => d.value || d.y || 0);
|
|
227
|
+
const maxValue = Math.max(...values);
|
|
228
|
+
const minValue = Math.min(0, Math.min(...values)); // Include 0 in scale
|
|
229
|
+
// Create scales
|
|
230
|
+
const xScale = d3Scale.scaleBand()
|
|
231
|
+
.domain(labels)
|
|
232
|
+
.range([vb.x, vb.x + vb.contentWidth])
|
|
233
|
+
.padding(0.2);
|
|
234
|
+
const yScale = d3Scale.scaleLinear()
|
|
235
|
+
.domain([minValue, maxValue])
|
|
236
|
+
.range([vb.y + vb.contentHeight, vb.y])
|
|
237
|
+
.nice();
|
|
238
|
+
// Create chart group
|
|
239
|
+
const chartGroup = doc.createElementNS(ns, 'g');
|
|
240
|
+
chartGroup.setAttribute('id', 'chart-content');
|
|
241
|
+
svg.appendChild(chartGroup);
|
|
242
|
+
// Draw grid if requested
|
|
243
|
+
if (showGrid) {
|
|
244
|
+
this.drawGrid(doc, chartGroup, xScale, yScale, vb, palette, 'vertical');
|
|
245
|
+
}
|
|
246
|
+
// Draw bars
|
|
247
|
+
data.forEach((d, i) => {
|
|
248
|
+
const value = d.value || d.y || 0;
|
|
249
|
+
const label = d.label || String(d.x || '');
|
|
250
|
+
const x = xScale(label);
|
|
251
|
+
const barWidth = xScale.bandwidth();
|
|
252
|
+
const barHeight = Math.abs(yScale(value) - yScale(0));
|
|
253
|
+
const y = value >= 0 ? yScale(value) : yScale(0);
|
|
254
|
+
const rect = doc.createElementNS(ns, 'rect');
|
|
255
|
+
rect.setAttribute('x', String(x));
|
|
256
|
+
rect.setAttribute('y', String(y));
|
|
257
|
+
rect.setAttribute('width', String(barWidth));
|
|
258
|
+
rect.setAttribute('height', String(barHeight));
|
|
259
|
+
rect.setAttribute('fill', (0, svg_theming_1.getColorForIndex)(i, branding.palette));
|
|
260
|
+
rect.setAttribute('stroke', 'none');
|
|
261
|
+
rect.setAttribute('rx', '2');
|
|
262
|
+
chartGroup.appendChild(rect);
|
|
263
|
+
// Add value label on top of bar
|
|
264
|
+
const valueText = doc.createElementNS(ns, 'text');
|
|
265
|
+
valueText.setAttribute('x', String(x + barWidth / 2));
|
|
266
|
+
valueText.setAttribute('y', String(y - 5));
|
|
267
|
+
valueText.setAttribute('text-anchor', 'middle');
|
|
268
|
+
valueText.setAttribute('font-family', font.family);
|
|
269
|
+
valueText.setAttribute('font-size', String(font.size - 2));
|
|
270
|
+
valueText.setAttribute('fill', palette.foreground);
|
|
271
|
+
valueText.textContent = String(Math.round(value * 100) / 100);
|
|
272
|
+
chartGroup.appendChild(valueText);
|
|
273
|
+
});
|
|
274
|
+
// Draw axes
|
|
275
|
+
this.drawXAxis(doc, svg, xScale, vb, palette, font, xAxisLabel);
|
|
276
|
+
this.drawYAxis(doc, svg, yScale, vb, palette, font, yAxisLabel);
|
|
277
|
+
// Add title
|
|
278
|
+
if (title) {
|
|
279
|
+
this.addTitle(doc, svg, title, viewBox.width, font);
|
|
280
|
+
}
|
|
281
|
+
return svg_utils_1.SVGUtils.sanitizeSVG(svg.outerHTML);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Renders a line chart
|
|
285
|
+
*/
|
|
286
|
+
async renderLineChart(data, viewBox, branding, title, xAxisLabel, yAxisLabel, showGrid, _showLegend) {
|
|
287
|
+
const vb = svg_utils_1.SVGUtils.calculateViewBox(viewBox);
|
|
288
|
+
const doc = svg_utils_1.SVGUtils.createSVG(viewBox.width, viewBox.height, 'line-chart');
|
|
289
|
+
const svg = doc.querySelector('svg');
|
|
290
|
+
const ns = svg.namespaceURI;
|
|
291
|
+
// Add accessibility and styles
|
|
292
|
+
if (title) {
|
|
293
|
+
svg_utils_1.SVGUtils.addA11y(svg, { title, ariaRole: 'img' });
|
|
294
|
+
}
|
|
295
|
+
svg_utils_1.SVGUtils.addStyles(svg, (0, svg_theming_1.generateCSS)(branding));
|
|
296
|
+
const palette = (0, svg_theming_1.getPalette)(branding.palette);
|
|
297
|
+
const font = (0, svg_theming_1.getFontSpec)(branding.font);
|
|
298
|
+
// Extract x and y values
|
|
299
|
+
const xValues = data.map(d => typeof d.x === 'number' ? d.x : parseFloat(String(d.x || 0)));
|
|
300
|
+
const yValues = data.map(d => d.y || d.value || 0);
|
|
301
|
+
// Create scales
|
|
302
|
+
const xScale = d3Scale.scaleLinear()
|
|
303
|
+
.domain([Math.min(...xValues), Math.max(...xValues)])
|
|
304
|
+
.range([vb.x, vb.x + vb.contentWidth])
|
|
305
|
+
.nice();
|
|
306
|
+
const yScale = d3Scale.scaleLinear()
|
|
307
|
+
.domain([Math.min(...yValues), Math.max(...yValues)])
|
|
308
|
+
.range([vb.y + vb.contentHeight, vb.y])
|
|
309
|
+
.nice();
|
|
310
|
+
// Create chart group
|
|
311
|
+
const chartGroup = doc.createElementNS(ns, 'g');
|
|
312
|
+
chartGroup.setAttribute('id', 'chart-content');
|
|
313
|
+
svg.appendChild(chartGroup);
|
|
314
|
+
// Draw grid if requested
|
|
315
|
+
if (showGrid) {
|
|
316
|
+
this.drawGrid(doc, chartGroup, xScale, yScale, vb, palette, 'both');
|
|
317
|
+
}
|
|
318
|
+
// Create line generator
|
|
319
|
+
const lineGenerator = d3Shape.line()
|
|
320
|
+
.x(d => xScale(typeof d.x === 'number' ? d.x : parseFloat(String(d.x || 0))))
|
|
321
|
+
.y(d => yScale(d.y || d.value || 0));
|
|
322
|
+
// Draw line
|
|
323
|
+
const linePath = doc.createElementNS(ns, 'path');
|
|
324
|
+
linePath.setAttribute('d', lineGenerator(data) || '');
|
|
325
|
+
linePath.setAttribute('fill', 'none');
|
|
326
|
+
linePath.setAttribute('stroke', (0, svg_theming_1.getColorForIndex)(0, branding.palette));
|
|
327
|
+
linePath.setAttribute('stroke-width', '3');
|
|
328
|
+
linePath.setAttribute('stroke-linecap', 'round');
|
|
329
|
+
linePath.setAttribute('stroke-linejoin', 'round');
|
|
330
|
+
chartGroup.appendChild(linePath);
|
|
331
|
+
// Draw data points
|
|
332
|
+
data.forEach((d) => {
|
|
333
|
+
const xVal = typeof d.x === 'number' ? d.x : parseFloat(String(d.x || 0));
|
|
334
|
+
const yVal = d.y || d.value || 0;
|
|
335
|
+
const circle = doc.createElementNS(ns, 'circle');
|
|
336
|
+
circle.setAttribute('cx', String(xScale(xVal)));
|
|
337
|
+
circle.setAttribute('cy', String(yScale(yVal)));
|
|
338
|
+
circle.setAttribute('r', '5');
|
|
339
|
+
circle.setAttribute('fill', (0, svg_theming_1.getColorForIndex)(0, branding.palette));
|
|
340
|
+
circle.setAttribute('stroke', '#fff');
|
|
341
|
+
circle.setAttribute('stroke-width', '2');
|
|
342
|
+
chartGroup.appendChild(circle);
|
|
343
|
+
});
|
|
344
|
+
// Draw axes
|
|
345
|
+
this.drawXAxisNumeric(doc, svg, xScale, vb, palette, font, xAxisLabel);
|
|
346
|
+
this.drawYAxis(doc, svg, yScale, vb, palette, font, yAxisLabel);
|
|
347
|
+
// Add title
|
|
348
|
+
if (title) {
|
|
349
|
+
this.addTitle(doc, svg, title, viewBox.width, font);
|
|
350
|
+
}
|
|
351
|
+
return svg_utils_1.SVGUtils.sanitizeSVG(svg.outerHTML);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Renders an area chart
|
|
355
|
+
*/
|
|
356
|
+
async renderAreaChart(data, viewBox, branding, title, xAxisLabel, yAxisLabel, showGrid, _showLegend) {
|
|
357
|
+
const vb = svg_utils_1.SVGUtils.calculateViewBox(viewBox);
|
|
358
|
+
const doc = svg_utils_1.SVGUtils.createSVG(viewBox.width, viewBox.height, 'area-chart');
|
|
359
|
+
const svg = doc.querySelector('svg');
|
|
360
|
+
const ns = svg.namespaceURI;
|
|
361
|
+
// Add accessibility and styles
|
|
362
|
+
if (title) {
|
|
363
|
+
svg_utils_1.SVGUtils.addA11y(svg, { title, ariaRole: 'img' });
|
|
364
|
+
}
|
|
365
|
+
svg_utils_1.SVGUtils.addStyles(svg, (0, svg_theming_1.generateCSS)(branding));
|
|
366
|
+
const palette = (0, svg_theming_1.getPalette)(branding.palette);
|
|
367
|
+
const font = (0, svg_theming_1.getFontSpec)(branding.font);
|
|
368
|
+
// Extract x and y values
|
|
369
|
+
const xValues = data.map(d => typeof d.x === 'number' ? d.x : parseFloat(String(d.x || 0)));
|
|
370
|
+
const yValues = data.map(d => d.y || d.value || 0);
|
|
371
|
+
// Create scales
|
|
372
|
+
const xScale = d3Scale.scaleLinear()
|
|
373
|
+
.domain([Math.min(...xValues), Math.max(...xValues)])
|
|
374
|
+
.range([vb.x, vb.x + vb.contentWidth])
|
|
375
|
+
.nice();
|
|
376
|
+
const yScale = d3Scale.scaleLinear()
|
|
377
|
+
.domain([0, Math.max(...yValues)]) // Start from 0 for area charts
|
|
378
|
+
.range([vb.y + vb.contentHeight, vb.y])
|
|
379
|
+
.nice();
|
|
380
|
+
// Create chart group
|
|
381
|
+
const chartGroup = doc.createElementNS(ns, 'g');
|
|
382
|
+
chartGroup.setAttribute('id', 'chart-content');
|
|
383
|
+
svg.appendChild(chartGroup);
|
|
384
|
+
// Draw grid if requested
|
|
385
|
+
if (showGrid) {
|
|
386
|
+
this.drawGrid(doc, chartGroup, xScale, yScale, vb, palette, 'both');
|
|
387
|
+
}
|
|
388
|
+
// Create area generator
|
|
389
|
+
const areaGenerator = d3Shape.area()
|
|
390
|
+
.x(d => xScale(typeof d.x === 'number' ? d.x : parseFloat(String(d.x || 0))))
|
|
391
|
+
.y0(yScale(0))
|
|
392
|
+
.y1(d => yScale(d.y || d.value || 0));
|
|
393
|
+
// Draw area
|
|
394
|
+
const areaPath = doc.createElementNS(ns, 'path');
|
|
395
|
+
areaPath.setAttribute('d', areaGenerator(data) || '');
|
|
396
|
+
const areaColor = (0, svg_theming_1.getColorForIndex)(0, branding.palette);
|
|
397
|
+
areaPath.setAttribute('fill', areaColor);
|
|
398
|
+
areaPath.setAttribute('fill-opacity', '0.3');
|
|
399
|
+
areaPath.setAttribute('stroke', 'none');
|
|
400
|
+
chartGroup.appendChild(areaPath);
|
|
401
|
+
// Draw line on top
|
|
402
|
+
const lineGenerator = d3Shape.line()
|
|
403
|
+
.x(d => xScale(typeof d.x === 'number' ? d.x : parseFloat(String(d.x || 0))))
|
|
404
|
+
.y(d => yScale(d.y || d.value || 0));
|
|
405
|
+
const linePath = doc.createElementNS(ns, 'path');
|
|
406
|
+
linePath.setAttribute('d', lineGenerator(data) || '');
|
|
407
|
+
linePath.setAttribute('fill', 'none');
|
|
408
|
+
linePath.setAttribute('stroke', areaColor);
|
|
409
|
+
linePath.setAttribute('stroke-width', '2');
|
|
410
|
+
chartGroup.appendChild(linePath);
|
|
411
|
+
// Draw axes
|
|
412
|
+
this.drawXAxisNumeric(doc, svg, xScale, vb, palette, font, xAxisLabel);
|
|
413
|
+
this.drawYAxis(doc, svg, yScale, vb, palette, font, yAxisLabel);
|
|
414
|
+
// Add title
|
|
415
|
+
if (title) {
|
|
416
|
+
this.addTitle(doc, svg, title, viewBox.width, font);
|
|
417
|
+
}
|
|
418
|
+
return svg_utils_1.SVGUtils.sanitizeSVG(svg.outerHTML);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Renders a pie chart
|
|
422
|
+
*/
|
|
423
|
+
async renderPieChart(data, viewBox, branding, title, showLegend) {
|
|
424
|
+
const vb = svg_utils_1.SVGUtils.calculateViewBox(viewBox);
|
|
425
|
+
const doc = svg_utils_1.SVGUtils.createSVG(viewBox.width, viewBox.height, 'pie-chart');
|
|
426
|
+
const svg = doc.querySelector('svg');
|
|
427
|
+
const ns = svg.namespaceURI;
|
|
428
|
+
// Add accessibility and styles
|
|
429
|
+
if (title) {
|
|
430
|
+
svg_utils_1.SVGUtils.addA11y(svg, { title, ariaRole: 'img' });
|
|
431
|
+
}
|
|
432
|
+
svg_utils_1.SVGUtils.addStyles(svg, (0, svg_theming_1.generateCSS)(branding));
|
|
433
|
+
const font = (0, svg_theming_1.getFontSpec)(branding.font);
|
|
434
|
+
// Calculate center and radius
|
|
435
|
+
const centerX = vb.x + vb.contentWidth / 2;
|
|
436
|
+
const centerY = vb.y + vb.contentHeight / 2;
|
|
437
|
+
const radius = Math.min(vb.contentWidth, vb.contentHeight) / 2 - 20;
|
|
438
|
+
// Create pie layout
|
|
439
|
+
const pieGenerator = d3Shape.pie()
|
|
440
|
+
.value(d => d.value || 0)
|
|
441
|
+
.sort(null);
|
|
442
|
+
const arcGenerator = d3Shape.arc()
|
|
443
|
+
.innerRadius(0)
|
|
444
|
+
.outerRadius(radius);
|
|
445
|
+
const labelArcGenerator = d3Shape.arc()
|
|
446
|
+
.innerRadius(radius * 0.6)
|
|
447
|
+
.outerRadius(radius * 0.6);
|
|
448
|
+
// Create chart group
|
|
449
|
+
const chartGroup = doc.createElementNS(ns, 'g');
|
|
450
|
+
chartGroup.setAttribute('transform', `translate(${centerX}, ${centerY})`);
|
|
451
|
+
svg.appendChild(chartGroup);
|
|
452
|
+
// Generate pie slices
|
|
453
|
+
const arcs = pieGenerator(data);
|
|
454
|
+
const total = data.reduce((sum, d) => sum + (d.value || 0), 0);
|
|
455
|
+
arcs.forEach((arc, i) => {
|
|
456
|
+
// Draw slice
|
|
457
|
+
const path = doc.createElementNS(ns, 'path');
|
|
458
|
+
path.setAttribute('d', arcGenerator(arc) || '');
|
|
459
|
+
path.setAttribute('fill', (0, svg_theming_1.getColorForIndex)(i, branding.palette));
|
|
460
|
+
path.setAttribute('stroke', '#fff');
|
|
461
|
+
path.setAttribute('stroke-width', '2');
|
|
462
|
+
chartGroup.appendChild(path);
|
|
463
|
+
// Add percentage label if slice is large enough
|
|
464
|
+
const percentage = ((arc.data.value || 0) / total) * 100;
|
|
465
|
+
if (percentage > 5) {
|
|
466
|
+
const centroid = labelArcGenerator.centroid(arc);
|
|
467
|
+
const text = doc.createElementNS(ns, 'text');
|
|
468
|
+
text.setAttribute('x', String(centroid[0]));
|
|
469
|
+
text.setAttribute('y', String(centroid[1]));
|
|
470
|
+
text.setAttribute('text-anchor', 'middle');
|
|
471
|
+
text.setAttribute('dominant-baseline', 'middle');
|
|
472
|
+
text.setAttribute('font-family', font.family);
|
|
473
|
+
text.setAttribute('font-size', String(font.size));
|
|
474
|
+
text.setAttribute('font-weight', 'bold');
|
|
475
|
+
text.setAttribute('fill', '#fff');
|
|
476
|
+
text.textContent = `${Math.round(percentage)}%`;
|
|
477
|
+
chartGroup.appendChild(text);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
// Add legend if requested
|
|
481
|
+
if (showLegend) {
|
|
482
|
+
this.addLegend(doc, svg, data, branding, vb, font);
|
|
483
|
+
}
|
|
484
|
+
// Add title
|
|
485
|
+
if (title) {
|
|
486
|
+
this.addTitle(doc, svg, title, viewBox.width, font);
|
|
487
|
+
}
|
|
488
|
+
return svg_utils_1.SVGUtils.sanitizeSVG(svg.outerHTML);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Renders a scatter plot
|
|
492
|
+
*/
|
|
493
|
+
async renderScatterChart(data, viewBox, branding, title, xAxisLabel, yAxisLabel, showGrid, _showLegend) {
|
|
494
|
+
const vb = svg_utils_1.SVGUtils.calculateViewBox(viewBox);
|
|
495
|
+
const doc = svg_utils_1.SVGUtils.createSVG(viewBox.width, viewBox.height, 'scatter-chart');
|
|
496
|
+
const svg = doc.querySelector('svg');
|
|
497
|
+
const ns = svg.namespaceURI;
|
|
498
|
+
// Add accessibility and styles
|
|
499
|
+
if (title) {
|
|
500
|
+
svg_utils_1.SVGUtils.addA11y(svg, { title, ariaRole: 'img' });
|
|
501
|
+
}
|
|
502
|
+
svg_utils_1.SVGUtils.addStyles(svg, (0, svg_theming_1.generateCSS)(branding));
|
|
503
|
+
const palette = (0, svg_theming_1.getPalette)(branding.palette);
|
|
504
|
+
const font = (0, svg_theming_1.getFontSpec)(branding.font);
|
|
505
|
+
// Extract x and y values
|
|
506
|
+
const xValues = data.map(d => typeof d.x === 'number' ? d.x : parseFloat(String(d.x || 0)));
|
|
507
|
+
const yValues = data.map(d => d.y || d.value || 0);
|
|
508
|
+
// Create scales
|
|
509
|
+
const xScale = d3Scale.scaleLinear()
|
|
510
|
+
.domain([Math.min(...xValues), Math.max(...xValues)])
|
|
511
|
+
.range([vb.x, vb.x + vb.contentWidth])
|
|
512
|
+
.nice();
|
|
513
|
+
const yScale = d3Scale.scaleLinear()
|
|
514
|
+
.domain([Math.min(...yValues), Math.max(...yValues)])
|
|
515
|
+
.range([vb.y + vb.contentHeight, vb.y])
|
|
516
|
+
.nice();
|
|
517
|
+
// Create chart group
|
|
518
|
+
const chartGroup = doc.createElementNS(ns, 'g');
|
|
519
|
+
chartGroup.setAttribute('id', 'chart-content');
|
|
520
|
+
svg.appendChild(chartGroup);
|
|
521
|
+
// Draw grid if requested
|
|
522
|
+
if (showGrid) {
|
|
523
|
+
this.drawGrid(doc, chartGroup, xScale, yScale, vb, palette, 'both');
|
|
524
|
+
}
|
|
525
|
+
// Draw scatter points
|
|
526
|
+
data.forEach((d, i) => {
|
|
527
|
+
const xVal = typeof d.x === 'number' ? d.x : parseFloat(String(d.x || 0));
|
|
528
|
+
const yVal = d.y || d.value || 0;
|
|
529
|
+
const circle = doc.createElementNS(ns, 'circle');
|
|
530
|
+
circle.setAttribute('cx', String(xScale(xVal)));
|
|
531
|
+
circle.setAttribute('cy', String(yScale(yVal)));
|
|
532
|
+
circle.setAttribute('r', '6');
|
|
533
|
+
circle.setAttribute('fill', (0, svg_theming_1.getColorForIndex)(i, branding.palette));
|
|
534
|
+
circle.setAttribute('fill-opacity', '0.7');
|
|
535
|
+
circle.setAttribute('stroke', (0, svg_theming_1.getColorForIndex)(i, branding.palette));
|
|
536
|
+
circle.setAttribute('stroke-width', '2');
|
|
537
|
+
chartGroup.appendChild(circle);
|
|
538
|
+
});
|
|
539
|
+
// Draw axes
|
|
540
|
+
this.drawXAxisNumeric(doc, svg, xScale, vb, palette, font, xAxisLabel);
|
|
541
|
+
this.drawYAxis(doc, svg, yScale, vb, palette, font, yAxisLabel);
|
|
542
|
+
// Add title
|
|
543
|
+
if (title) {
|
|
544
|
+
this.addTitle(doc, svg, title, viewBox.width, font);
|
|
545
|
+
}
|
|
546
|
+
return svg_utils_1.SVGUtils.sanitizeSVG(svg.outerHTML);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Draws grid lines
|
|
550
|
+
*/
|
|
551
|
+
drawGrid(doc, container, xScale, yScale, vb, palette, direction) {
|
|
552
|
+
const ns = container.namespaceURI;
|
|
553
|
+
const gridGroup = doc.createElementNS(ns, 'g');
|
|
554
|
+
gridGroup.setAttribute('class', 'grid');
|
|
555
|
+
container.appendChild(gridGroup);
|
|
556
|
+
// Horizontal grid lines (for Y axis)
|
|
557
|
+
if (direction === 'horizontal' || direction === 'both') {
|
|
558
|
+
const yTicks = yScale.ticks(5);
|
|
559
|
+
yTicks.forEach(tick => {
|
|
560
|
+
const line = doc.createElementNS(ns, 'line');
|
|
561
|
+
line.setAttribute('x1', String(vb.x));
|
|
562
|
+
line.setAttribute('y1', String(yScale(tick)));
|
|
563
|
+
line.setAttribute('x2', String(vb.x + vb.contentWidth));
|
|
564
|
+
line.setAttribute('y2', String(yScale(tick)));
|
|
565
|
+
line.setAttribute('stroke', palette.foreground);
|
|
566
|
+
line.setAttribute('stroke-opacity', '0.1');
|
|
567
|
+
line.setAttribute('stroke-dasharray', '2,2');
|
|
568
|
+
gridGroup.appendChild(line);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// Vertical grid lines (for X axis)
|
|
572
|
+
if (direction === 'vertical' || direction === 'both') {
|
|
573
|
+
if (typeof xScale.ticks === 'function') {
|
|
574
|
+
const xTicks = xScale.ticks(5);
|
|
575
|
+
xTicks.forEach((tick) => {
|
|
576
|
+
const line = doc.createElementNS(ns, 'line');
|
|
577
|
+
line.setAttribute('x1', String(xScale(tick)));
|
|
578
|
+
line.setAttribute('y1', String(vb.y));
|
|
579
|
+
line.setAttribute('x2', String(xScale(tick)));
|
|
580
|
+
line.setAttribute('y2', String(vb.y + vb.contentHeight));
|
|
581
|
+
line.setAttribute('stroke', palette.foreground);
|
|
582
|
+
line.setAttribute('stroke-opacity', '0.1');
|
|
583
|
+
line.setAttribute('stroke-dasharray', '2,2');
|
|
584
|
+
gridGroup.appendChild(line);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Draws X-axis for categorical data
|
|
591
|
+
*/
|
|
592
|
+
drawXAxis(doc, svg, xScale, vb, palette, font, label) {
|
|
593
|
+
const ns = svg.namespaceURI;
|
|
594
|
+
const axisGroup = doc.createElementNS(ns, 'g');
|
|
595
|
+
axisGroup.setAttribute('class', 'x-axis');
|
|
596
|
+
svg.appendChild(axisGroup);
|
|
597
|
+
// Draw axis line
|
|
598
|
+
const axisLine = doc.createElementNS(ns, 'line');
|
|
599
|
+
axisLine.setAttribute('x1', String(vb.x));
|
|
600
|
+
axisLine.setAttribute('y1', String(vb.y + vb.contentHeight));
|
|
601
|
+
axisLine.setAttribute('x2', String(vb.x + vb.contentWidth));
|
|
602
|
+
axisLine.setAttribute('y2', String(vb.y + vb.contentHeight));
|
|
603
|
+
axisLine.setAttribute('stroke', palette.foreground);
|
|
604
|
+
axisLine.setAttribute('stroke-width', '2');
|
|
605
|
+
axisGroup.appendChild(axisLine);
|
|
606
|
+
// Draw labels
|
|
607
|
+
xScale.domain().forEach(label => {
|
|
608
|
+
const x = xScale(label) + xScale.bandwidth() / 2;
|
|
609
|
+
const text = doc.createElementNS(ns, 'text');
|
|
610
|
+
text.setAttribute('x', String(x));
|
|
611
|
+
text.setAttribute('y', String(vb.y + vb.contentHeight + 20));
|
|
612
|
+
text.setAttribute('text-anchor', 'middle');
|
|
613
|
+
text.setAttribute('font-family', font.family);
|
|
614
|
+
text.setAttribute('font-size', String(font.size));
|
|
615
|
+
text.setAttribute('fill', palette.foreground);
|
|
616
|
+
text.textContent = label;
|
|
617
|
+
axisGroup.appendChild(text);
|
|
618
|
+
});
|
|
619
|
+
// Add axis label if provided
|
|
620
|
+
if (label) {
|
|
621
|
+
const labelText = doc.createElementNS(ns, 'text');
|
|
622
|
+
labelText.setAttribute('x', String(vb.x + vb.contentWidth / 2));
|
|
623
|
+
labelText.setAttribute('y', String(vb.y + vb.contentHeight + 50));
|
|
624
|
+
labelText.setAttribute('text-anchor', 'middle');
|
|
625
|
+
labelText.setAttribute('font-family', font.family);
|
|
626
|
+
labelText.setAttribute('font-size', String(font.size + 2));
|
|
627
|
+
labelText.setAttribute('font-weight', '600');
|
|
628
|
+
labelText.setAttribute('fill', palette.foreground);
|
|
629
|
+
labelText.textContent = label;
|
|
630
|
+
axisGroup.appendChild(labelText);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Draws X-axis for numeric data
|
|
635
|
+
*/
|
|
636
|
+
drawXAxisNumeric(doc, svg, xScale, vb, palette, font, label) {
|
|
637
|
+
const ns = svg.namespaceURI;
|
|
638
|
+
const axisGroup = doc.createElementNS(ns, 'g');
|
|
639
|
+
axisGroup.setAttribute('class', 'x-axis');
|
|
640
|
+
svg.appendChild(axisGroup);
|
|
641
|
+
// Draw axis line
|
|
642
|
+
const axisLine = doc.createElementNS(ns, 'line');
|
|
643
|
+
axisLine.setAttribute('x1', String(vb.x));
|
|
644
|
+
axisLine.setAttribute('y1', String(vb.y + vb.contentHeight));
|
|
645
|
+
axisLine.setAttribute('x2', String(vb.x + vb.contentWidth));
|
|
646
|
+
axisLine.setAttribute('y2', String(vb.y + vb.contentHeight));
|
|
647
|
+
axisLine.setAttribute('stroke', palette.foreground);
|
|
648
|
+
axisLine.setAttribute('stroke-width', '2');
|
|
649
|
+
axisGroup.appendChild(axisLine);
|
|
650
|
+
// Draw ticks and labels
|
|
651
|
+
const ticks = xScale.ticks(5);
|
|
652
|
+
ticks.forEach(tick => {
|
|
653
|
+
const x = xScale(tick);
|
|
654
|
+
// Tick mark
|
|
655
|
+
const tickLine = doc.createElementNS(ns, 'line');
|
|
656
|
+
tickLine.setAttribute('x1', String(x));
|
|
657
|
+
tickLine.setAttribute('y1', String(vb.y + vb.contentHeight));
|
|
658
|
+
tickLine.setAttribute('x2', String(x));
|
|
659
|
+
tickLine.setAttribute('y2', String(vb.y + vb.contentHeight + 5));
|
|
660
|
+
tickLine.setAttribute('stroke', palette.foreground);
|
|
661
|
+
tickLine.setAttribute('stroke-width', '1');
|
|
662
|
+
axisGroup.appendChild(tickLine);
|
|
663
|
+
// Label
|
|
664
|
+
const text = doc.createElementNS(ns, 'text');
|
|
665
|
+
text.setAttribute('x', String(x));
|
|
666
|
+
text.setAttribute('y', String(vb.y + vb.contentHeight + 20));
|
|
667
|
+
text.setAttribute('text-anchor', 'middle');
|
|
668
|
+
text.setAttribute('font-family', font.family);
|
|
669
|
+
text.setAttribute('font-size', String(font.size));
|
|
670
|
+
text.setAttribute('fill', palette.foreground);
|
|
671
|
+
text.textContent = String(Math.round(tick * 100) / 100);
|
|
672
|
+
axisGroup.appendChild(text);
|
|
673
|
+
});
|
|
674
|
+
// Add axis label if provided
|
|
675
|
+
if (label) {
|
|
676
|
+
const labelText = doc.createElementNS(ns, 'text');
|
|
677
|
+
labelText.setAttribute('x', String(vb.x + vb.contentWidth / 2));
|
|
678
|
+
labelText.setAttribute('y', String(vb.y + vb.contentHeight + 50));
|
|
679
|
+
labelText.setAttribute('text-anchor', 'middle');
|
|
680
|
+
labelText.setAttribute('font-family', font.family);
|
|
681
|
+
labelText.setAttribute('font-size', String(font.size + 2));
|
|
682
|
+
labelText.setAttribute('font-weight', '600');
|
|
683
|
+
labelText.setAttribute('fill', palette.foreground);
|
|
684
|
+
labelText.textContent = label;
|
|
685
|
+
axisGroup.appendChild(labelText);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Draws Y-axis
|
|
690
|
+
*/
|
|
691
|
+
drawYAxis(doc, svg, yScale, vb, palette, font, label) {
|
|
692
|
+
const ns = svg.namespaceURI;
|
|
693
|
+
const axisGroup = doc.createElementNS(ns, 'g');
|
|
694
|
+
axisGroup.setAttribute('class', 'y-axis');
|
|
695
|
+
svg.appendChild(axisGroup);
|
|
696
|
+
// Draw axis line
|
|
697
|
+
const axisLine = doc.createElementNS(ns, 'line');
|
|
698
|
+
axisLine.setAttribute('x1', String(vb.x));
|
|
699
|
+
axisLine.setAttribute('y1', String(vb.y));
|
|
700
|
+
axisLine.setAttribute('x2', String(vb.x));
|
|
701
|
+
axisLine.setAttribute('y2', String(vb.y + vb.contentHeight));
|
|
702
|
+
axisLine.setAttribute('stroke', palette.foreground);
|
|
703
|
+
axisLine.setAttribute('stroke-width', '2');
|
|
704
|
+
axisGroup.appendChild(axisLine);
|
|
705
|
+
// Draw ticks and labels
|
|
706
|
+
const ticks = yScale.ticks(5);
|
|
707
|
+
ticks.forEach(tick => {
|
|
708
|
+
const y = yScale(tick);
|
|
709
|
+
// Tick mark
|
|
710
|
+
const tickLine = doc.createElementNS(ns, 'line');
|
|
711
|
+
tickLine.setAttribute('x1', String(vb.x - 5));
|
|
712
|
+
tickLine.setAttribute('y1', String(y));
|
|
713
|
+
tickLine.setAttribute('x2', String(vb.x));
|
|
714
|
+
tickLine.setAttribute('y2', String(y));
|
|
715
|
+
tickLine.setAttribute('stroke', palette.foreground);
|
|
716
|
+
tickLine.setAttribute('stroke-width', '1');
|
|
717
|
+
axisGroup.appendChild(tickLine);
|
|
718
|
+
// Label
|
|
719
|
+
const text = doc.createElementNS(ns, 'text');
|
|
720
|
+
text.setAttribute('x', String(vb.x - 10));
|
|
721
|
+
text.setAttribute('y', String(y));
|
|
722
|
+
text.setAttribute('text-anchor', 'end');
|
|
723
|
+
text.setAttribute('dominant-baseline', 'middle');
|
|
724
|
+
text.setAttribute('font-family', font.family);
|
|
725
|
+
text.setAttribute('font-size', String(font.size));
|
|
726
|
+
text.setAttribute('fill', palette.foreground);
|
|
727
|
+
text.textContent = String(Math.round(tick * 100) / 100);
|
|
728
|
+
axisGroup.appendChild(text);
|
|
729
|
+
});
|
|
730
|
+
// Add axis label if provided
|
|
731
|
+
if (label) {
|
|
732
|
+
const labelText = doc.createElementNS(ns, 'text');
|
|
733
|
+
labelText.setAttribute('x', String(vb.x - 50));
|
|
734
|
+
labelText.setAttribute('y', String(vb.y + vb.contentHeight / 2));
|
|
735
|
+
labelText.setAttribute('text-anchor', 'middle');
|
|
736
|
+
labelText.setAttribute('font-family', font.family);
|
|
737
|
+
labelText.setAttribute('font-size', String(font.size + 2));
|
|
738
|
+
labelText.setAttribute('font-weight', '600');
|
|
739
|
+
labelText.setAttribute('fill', palette.foreground);
|
|
740
|
+
labelText.setAttribute('transform', `rotate(-90, ${vb.x - 50}, ${vb.y + vb.contentHeight / 2})`);
|
|
741
|
+
labelText.textContent = label;
|
|
742
|
+
axisGroup.appendChild(labelText);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Adds a title to the chart
|
|
747
|
+
*/
|
|
748
|
+
addTitle(doc, svg, title, width, font) {
|
|
749
|
+
const ns = svg.namespaceURI;
|
|
750
|
+
const text = doc.createElementNS(ns, 'text');
|
|
751
|
+
text.setAttribute('x', String(width / 2));
|
|
752
|
+
text.setAttribute('y', '30');
|
|
753
|
+
text.setAttribute('text-anchor', 'middle');
|
|
754
|
+
text.setAttribute('font-family', font.family);
|
|
755
|
+
text.setAttribute('font-size', String(font.size + 6));
|
|
756
|
+
text.setAttribute('font-weight', 'bold');
|
|
757
|
+
text.textContent = title;
|
|
758
|
+
svg.appendChild(text);
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Adds a legend for pie charts
|
|
762
|
+
*/
|
|
763
|
+
addLegend(doc, svg, data, branding, vb, font) {
|
|
764
|
+
const ns = svg.namespaceURI;
|
|
765
|
+
const legendGroup = doc.createElementNS(ns, 'g');
|
|
766
|
+
legendGroup.setAttribute('class', 'legend');
|
|
767
|
+
legendGroup.setAttribute('transform', `translate(${vb.x + vb.contentWidth + 20}, ${vb.y})`);
|
|
768
|
+
svg.appendChild(legendGroup);
|
|
769
|
+
data.forEach((d, i) => {
|
|
770
|
+
const g = doc.createElementNS(ns, 'g');
|
|
771
|
+
g.setAttribute('transform', `translate(0, ${i * 25})`);
|
|
772
|
+
// Color box
|
|
773
|
+
const rect = doc.createElementNS(ns, 'rect');
|
|
774
|
+
rect.setAttribute('width', '15');
|
|
775
|
+
rect.setAttribute('height', '15');
|
|
776
|
+
rect.setAttribute('fill', (0, svg_theming_1.getColorForIndex)(i, branding.palette));
|
|
777
|
+
g.appendChild(rect);
|
|
778
|
+
// Label
|
|
779
|
+
const text = doc.createElementNS(ns, 'text');
|
|
780
|
+
text.setAttribute('x', '20');
|
|
781
|
+
text.setAttribute('y', '12');
|
|
782
|
+
text.setAttribute('font-family', font.family);
|
|
783
|
+
text.setAttribute('font-size', String(font.size));
|
|
784
|
+
text.textContent = d.label || `Item ${i + 1}`;
|
|
785
|
+
g.appendChild(text);
|
|
786
|
+
legendGroup.appendChild(g);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Helper to safely parse JSON
|
|
791
|
+
*/
|
|
792
|
+
parseJSON(value, paramName) {
|
|
793
|
+
if (typeof value === 'object' && value !== null) {
|
|
794
|
+
return value;
|
|
795
|
+
}
|
|
796
|
+
if (typeof value === 'string') {
|
|
797
|
+
try {
|
|
798
|
+
return JSON.parse(value);
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
throw new Error(`Parameter '${paramName}' contains invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
throw new Error(`Parameter '${paramName}' must be a JSON string or object. Received ${typeof value}.`);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Helper to ensure a parameter value is a string
|
|
808
|
+
*/
|
|
809
|
+
ensureString(value, paramName) {
|
|
810
|
+
if (value == null) {
|
|
811
|
+
return '';
|
|
812
|
+
}
|
|
813
|
+
if (typeof value === 'string') {
|
|
814
|
+
return value;
|
|
815
|
+
}
|
|
816
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
817
|
+
return String(value);
|
|
818
|
+
}
|
|
819
|
+
throw new Error(`Parameter '${paramName}' must be a string, number, or boolean. ` +
|
|
820
|
+
`Received ${typeof value}. If providing JSON data, ensure it's passed as a string.`);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Helper to get parameter value by name (case-insensitive)
|
|
824
|
+
*/
|
|
825
|
+
getParamValue(params, paramName) {
|
|
826
|
+
const param = params.Params.find(p => p.Name.trim().toLowerCase() === paramName.toLowerCase());
|
|
827
|
+
if (param?.Value && typeof param.Value === 'string') {
|
|
828
|
+
return param?.Value?.trim() || null;
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
return param?.Value || null;
|
|
832
|
+
}
|
|
133
833
|
}
|
|
134
834
|
};
|
|
135
835
|
exports.CreateSVGChartAction = CreateSVGChartAction;
|