smithkit 0.2.0 → 0.4.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.
Files changed (48) hide show
  1. package/README.md +318 -25
  2. package/dist/lib/OptionsValidation.d.ts +16 -0
  3. package/dist/lib/OptionsValidation.js +168 -0
  4. package/dist/lib/Smith.d.ts +24 -39
  5. package/dist/lib/Smith.js +175 -287
  6. package/dist/lib/appearance/SvgTheme.d.ts +6 -0
  7. package/dist/lib/appearance/SvgTheme.js +21 -0
  8. package/dist/lib/appearance/Theme.d.ts +8 -0
  9. package/dist/lib/appearance/Theme.js +125 -0
  10. package/dist/lib/appearance/types.d.ts +55 -0
  11. package/dist/lib/appearance/types.js +1 -0
  12. package/dist/lib/grid/CircleLayerControl.d.ts +1 -1
  13. package/dist/lib/grid/CircleLayerControl.js +10 -8
  14. package/dist/lib/grid/ConstCircles.d.ts +0 -3
  15. package/dist/lib/grid/ConstCircles.js +6 -20
  16. package/dist/lib/grid/ConstQCircles.d.ts +1 -0
  17. package/dist/lib/grid/ConstQCircles.js +6 -1
  18. package/dist/lib/grid/ConstSwrCircles.d.ts +1 -0
  19. package/dist/lib/grid/ConstSwrCircles.js +6 -1
  20. package/dist/lib/grid/GridDefinitions.d.ts +7 -1
  21. package/dist/lib/grid/GridDefinitions.js +29 -0
  22. package/dist/lib/grid/GridLayerControl.d.ts +5 -4
  23. package/dist/lib/grid/GridLayerControl.js +13 -3
  24. package/dist/lib/grid/LayerControl.d.ts +1 -2
  25. package/dist/lib/grid/LayerControl.js +1 -10
  26. package/dist/lib/grid/SmithGridLayer.d.ts +7 -1
  27. package/dist/lib/grid/SmithGridLayer.js +35 -5
  28. package/dist/lib/index.d.ts +4 -1
  29. package/dist/lib/interaction/SmithCursor.d.ts +0 -17
  30. package/dist/lib/interaction/SmithCursor.js +6 -14
  31. package/dist/lib/layers.d.ts +7 -1
  32. package/dist/lib/options.d.ts +54 -0
  33. package/dist/lib/options.js +1 -0
  34. package/dist/lib/rendering/SvgChartRenderer.d.ts +69 -0
  35. package/dist/lib/rendering/SvgChartRenderer.js +364 -0
  36. package/dist/lib/scales/SmithPeripheralScales.js +11 -11
  37. package/dist/lib/scales/SmithScales.d.ts +10 -3
  38. package/dist/lib/scales/SmithScales.js +73 -39
  39. package/dist/lib/svg/ImageExporter.d.ts +18 -0
  40. package/dist/lib/svg/ImageExporter.js +211 -0
  41. package/dist/lib/svg/MarkerLegend.d.ts +12 -0
  42. package/dist/lib/svg/MarkerLegend.js +63 -0
  43. package/dist/lib/svg/SvgExporter.d.ts +6 -1
  44. package/dist/lib/svg/SvgExporter.js +50 -5
  45. package/dist/lib/svg/export.d.ts +36 -0
  46. package/dist/lib/svg/export.js +1 -0
  47. package/dist/lib/traces/SmithMarker.js +8 -5
  48. package/package.json +1 -1
package/dist/lib/Smith.js CHANGED
@@ -1,25 +1,10 @@
1
- import * as d3 from 'd3';
2
- import { MouseGesture } from './interaction/MouseGesture.js';
3
- import { SmithSvg } from './svg/SmithSvg.js';
4
- import { LabelLayout } from './svg/LabelLayout.js';
5
- import { SvgExporter } from './svg/SvgExporter.js';
6
- import { SmithGroup } from './svg/SmithGroup.js';
7
- import { SmithCircle } from './svg/SmithCircle.js';
8
- import { SmithData } from './traces/SmithData.js';
9
- import { SmithCursor } from './interaction/SmithCursor.js';
10
- import { ConstResistance } from './grid/ConstResistance.js';
11
- import { ConstReactance } from './grid/ConstReactance.js';
12
- import { ConstConductance } from './grid/ConstConductance.js';
13
- import { ConstSusceptance } from './grid/ConstSusceptance.js';
14
- import { ConstQCircles } from './grid/ConstQCircles.js';
15
- import { ConstSwrCircles } from './grid/ConstSwrCircles.js';
16
- import { SmithScaler } from './svg/SmithScaler.js';
17
- import { SmithConstantCircle } from './rf/SmithConstantCircle.js';
18
- import { GridDefinitions } from './grid/GridDefinitions.js';
1
+ import { OptionsValidation } from './OptionsValidation.js';
2
+ import { MarkerLegend } from './svg/MarkerLegend.js';
3
+ import { ImageExporter } from './svg/ImageExporter.js';
4
+ import { Theme } from './appearance/Theme.js';
5
+ import { color as parseColor } from 'd3';
6
+ import { SvgChartRenderer } from './rendering/SvgChartRenderer.js';
19
7
  import { Complex } from './math/Complex.js';
20
- import { SmithPeripheralScales } from './scales/SmithPeripheralScales.js';
21
- import { GridLayerControl } from './grid/GridLayerControl.js';
22
- import { CircleLayerControl } from './grid/CircleLayerControl.js';
23
8
  import { RfCalculations } from './rf/RfCalculations.js';
24
9
  import { MarkerMeasurements } from './MarkerMeasurements.js';
25
10
  export var SmithEventType;
@@ -31,207 +16,189 @@ export var SmithEventType;
31
16
  SmithEventType["MarkerDragEnd"] = "marker-drag-end";
32
17
  })(SmithEventType || (SmithEventType = {}));
33
18
  export class Smith {
19
+ theme;
34
20
  referenceOhms;
35
- calcs = new SmithConstantCircle();
36
- scalers;
37
- defaultTransform = d3.zoomIdentity.translate(62.5, 62.5).scale(0.75);
38
- transform = d3.zoomIdentity;
39
- zoomEnabled = true;
40
- applyingView = false;
41
- zoomBehavior = d3.zoom();
42
- svg;
43
- resizeObserver;
44
- labelLayout;
45
- container;
46
- dataContainer;
47
- markerContainer;
48
- reactanceAxis;
49
- constResistance;
50
- constReactance;
51
- constConductance;
52
- constSusceptance;
53
- constSwrCircles;
54
- constQCircles;
55
- cursor;
56
- peripheralScaleRenderer = new SmithPeripheralScales();
21
+ renderer;
57
22
  data = [];
58
23
  destroyed = false;
59
- mouseGesture = new MouseGesture();
60
24
  nextDatasetColor = 0;
61
25
  nextTraceId = 1;
62
26
  nextMarkerId = 1;
63
27
  traceMetadata = new WeakMap();
64
28
  markerIds = new WeakMap();
65
29
  draggedMarkers = new Set();
66
- cursorBeforeMarkerDrag = null;
67
30
  listeners = new Set();
68
31
  layers;
69
32
  peripheralScales;
70
- constructor(referenceOhms = 50) {
71
- this.referenceOhms = referenceOhms;
72
- if (!Number.isFinite(referenceOhms) || referenceOhms <= 0) {
33
+ constructor(options = {}) {
34
+ OptionsValidation.chart(options);
35
+ this.referenceOhms =
36
+ options.referenceImpedanceOhms === undefined ? 50 : options.referenceImpedanceOhms;
37
+ if (!Number.isFinite(this.referenceOhms) || this.referenceOhms <= 0) {
73
38
  throw new RangeError('Reference impedance must be positive and finite.');
74
39
  }
75
- const viewBoxSize = 500;
76
- const gridData = GridDefinitions.create();
77
- this.scalers = this.createScalers(viewBoxSize);
78
- this.svg = new SmithSvg(viewBoxSize);
79
- this.container = new SmithGroup();
80
- this.constResistance = new ConstResistance({
81
- data: gridData,
82
- scaler: this.scalers.default,
83
- showMinor: true,
40
+ this.theme = Theme.resolve(options.appearance);
41
+ this.renderer = new SvgChartRenderer((position) => {
42
+ this.emit({
43
+ type: SmithEventType.Cursor,
44
+ data: position ? RfCalculations.readReflection(position, this.referenceOhms) : undefined,
45
+ });
84
46
  });
85
- this.constResistance.show();
86
- this.constReactance = new ConstReactance({
87
- data: gridData,
88
- scaler: this.scalers.default,
89
- showMinor: true,
90
- });
91
- this.constReactance.show();
92
- this.constConductance = new ConstConductance({
93
- data: gridData,
94
- scaler: this.scalers.default,
95
- showMinor: true,
96
- });
97
- this.constConductance.hide();
98
- this.constSusceptance = new ConstSusceptance({
99
- data: gridData,
100
- scaler: this.scalers.default,
101
- showMinor: true,
102
- });
103
- this.constSusceptance.hide();
104
- this.constQCircles = new ConstQCircles(this.scalers.default);
105
- this.constQCircles.hide();
106
- this.constSwrCircles = new ConstSwrCircles(this.scalers.default);
107
- this.constSwrCircles.hide();
108
- this.cursor = this.initCursor();
109
- const cursorContainer = this.cursorContainer();
110
- this.reactanceAxis = this.drawReactanceAxis({
111
- stroke: '#334155',
112
- strokeWidth: '1',
113
- fill: 'none',
114
- });
115
- this.dataContainer = new SmithGroup().attr('data-layer', 'samples');
116
- this.markerContainer = new SmithGroup().attr('data-layer', 'markers');
117
- // build chart
118
- this.svg.append(this.container);
119
- this.container.append(this.constConductance.draw().attr('data-layer', 'conductance'));
120
- this.container.append(this.constSusceptance.draw().attr('data-layer', 'susceptance'));
121
- this.container.append(this.constResistance.draw().attr('data-layer', 'resistance'));
122
- this.container.append(this.constReactance.draw().attr('data-layer', 'reactance'));
123
- this.container.append(this.constQCircles.draw());
124
- this.container.append(this.constSwrCircles.draw());
125
- this.container.append(this.peripheralScaleRenderer);
126
- this.container.append(this.cursor.Group);
127
- this.container.append(this.reactanceAxis);
128
- this.container.append(cursorContainer);
129
- this.container.append(this.dataContainer);
130
- const labels = new SmithGroup().attr('data-layer', 'labels').attr('pointer-events', 'none');
131
- for (const [name, layer] of [
132
- ['conductance', this.constConductance],
133
- ['susceptance', this.constSusceptance],
134
- ['resistance', this.constResistance],
135
- ['reactance', this.constReactance],
136
- ]) {
137
- labels.append(layer.labels.attr('data-label-layer', name));
138
- }
139
- labels.append(this.peripheralScaleRenderer.labels);
140
- this.container.append(labels);
141
- this.container.append(this.markerContainer);
142
- this.labelLayout = new LabelLayout(this.svg.Node, labels.Node, this.defaultTransform);
143
- const assertAlive = () => this.assertAlive();
144
- this.layers = {
145
- resistance: new GridLayerControl(this.constResistance, assertAlive, () => this.labelLayout.update(true)),
146
- reactance: new GridLayerControl(this.constReactance, assertAlive, () => this.labelLayout.update(true)),
147
- conductance: new GridLayerControl(this.constConductance, assertAlive, () => this.labelLayout.update(true)),
148
- susceptance: new GridLayerControl(this.constSusceptance, assertAlive, () => this.labelLayout.update(true)),
149
- q: new CircleLayerControl(this.constQCircles, 0, assertAlive),
150
- vswr: new CircleLayerControl(this.constSwrCircles, 1, assertAlive),
151
- };
152
- this.peripheralScales = {
153
- setVisible: (visible) => {
154
- assertAlive();
155
- if (visible) {
156
- this.peripheralScaleRenderer.show();
47
+ this.renderer.setTheme(this.theme);
48
+ this.layers = this.renderer.layers;
49
+ this.peripheralScales = this.renderer.peripheralScales;
50
+ try {
51
+ this.applyOptions(options);
52
+ }
53
+ catch (error) {
54
+ // Invalid constructor options must not retain observers or event handlers.
55
+ this.renderer.destroy();
56
+ throw error;
57
+ }
58
+ }
59
+ /** Apply a validated patch. Omitted fields retain their current settings. */
60
+ setOptions(options) {
61
+ this.assertAlive();
62
+ OptionsValidation.chart(options);
63
+ // Renormalization can reject otherwise valid options for the loaded samples.
64
+ // Do it before presentation changes so rejection preserves the entire chart.
65
+ if (options.referenceImpedanceOhms !== undefined) {
66
+ this.renormalize(options.referenceImpedanceOhms);
67
+ }
68
+ if (options.appearance !== undefined) {
69
+ this.setAppearance(options.appearance);
70
+ }
71
+ this.applyOptions(options);
72
+ }
73
+ applyOptions(options) {
74
+ if (options.interaction?.zoom !== undefined) {
75
+ this.setZoomEnabled(options.interaction.zoom);
76
+ }
77
+ if (options.interaction?.cursor !== undefined) {
78
+ this.setCursorEnabled(options.interaction.cursor);
79
+ }
80
+ if (options.peripheralScales?.visible !== undefined) {
81
+ this.peripheralScales.setVisible(options.peripheralScales.visible);
82
+ }
83
+ for (const name of ['resistance', 'reactance', 'conductance', 'susceptance']) {
84
+ const layer = this.layers[name];
85
+ for (const settings of [options.grid, options.grid?.layers?.[name]]) {
86
+ if (settings === undefined) {
87
+ continue;
157
88
  }
158
- else {
159
- this.peripheralScaleRenderer.hide();
89
+ if (settings.detail !== undefined) {
90
+ layer.setDetail(settings.detail);
160
91
  }
161
- this.labelLayout.update(true);
162
- },
163
- update: (gamma) => {
164
- assertAlive();
165
- this.peripheralScaleRenderer.update(gamma);
166
- },
167
- };
168
- this.initializeZoom();
169
- this.resizeObserver = new ResizeObserver(() => this.updateViewportScale());
170
- this.resizeObserver.observe(this.svg.Node);
92
+ if (settings.labelsVisible !== undefined) {
93
+ layer.setLabelsVisible(settings.labelsVisible);
94
+ }
95
+ if (settings.style !== undefined) {
96
+ layer.setStyle(settings.style);
97
+ }
98
+ }
99
+ const visible = options.grid?.layers?.[name]?.visible;
100
+ if (visible !== undefined) {
101
+ layer.setVisible(visible);
102
+ }
103
+ }
104
+ for (const name of ['q', 'vswr']) {
105
+ const settings = options.circles?.[name];
106
+ if (settings === undefined) {
107
+ continue;
108
+ }
109
+ const layer = this.layers[name];
110
+ if (settings.values !== undefined) {
111
+ layer.setValues(settings.values);
112
+ }
113
+ if (settings.style !== undefined) {
114
+ layer.setStyle(settings.style);
115
+ }
116
+ if (settings.visible !== undefined) {
117
+ layer.setVisible(settings.visible);
118
+ }
119
+ }
120
+ }
121
+ /** Replace the appearance while retaining explicit layer styles and trace colors. */
122
+ setAppearance(appearance) {
123
+ this.assertAlive();
124
+ const theme = Theme.resolve(appearance);
125
+ this.theme = theme;
126
+ this.renderer.setTheme(theme);
127
+ for (const data of this.data) {
128
+ const index = this.traceMetadata.get(data).colorIndex;
129
+ if (index !== undefined) {
130
+ data.setColor(theme.traceColors[index % theme.traceColors.length]);
131
+ }
132
+ }
171
133
  }
172
134
  draw(target) {
173
135
  this.assertAlive();
174
- const host = typeof target === 'string' ? document.querySelector(target) : target;
175
- if (!host) {
176
- throw new Error('Chart container was not found.');
136
+ this.renderer.draw(target);
137
+ }
138
+ /** Set the same detail level on all impedance and admittance grid layers. */
139
+ setGridDetail(detail) {
140
+ this.assertAlive();
141
+ for (const name of ['resistance', 'reactance', 'conductance', 'susceptance']) {
142
+ this.layers[name].setDetail(detail);
177
143
  }
178
- host.appendChild(this.svg.Node);
179
- this.updateViewportScale();
180
144
  }
181
- /** Export the current mounted view as standalone SVG with a transparent background. */
182
- toSvg() {
145
+ /** Export the current mounted view as standalone SVG with its configured background. */
146
+ toSvg(options) {
183
147
  this.assertAlive();
184
- return SvgExporter.chart(this.svg.Node);
148
+ this.validateExportOptions(options);
149
+ const source = this.renderer.toSvg();
150
+ if (options === undefined) {
151
+ return source;
152
+ }
153
+ const sources = [source];
154
+ if (options.scales) {
155
+ sources.push(options.scales.toSvg({ readout: options.scaleReadout }));
156
+ }
157
+ return ImageExporter.svg(sources, options, this.exportLegend(options));
158
+ }
159
+ validateExportOptions(options) {
160
+ if (options?.scaleReadout !== undefined && !options.scales) {
161
+ throw new TypeError('Scale readout requires included radial scales.');
162
+ }
163
+ }
164
+ exportLegend(options) {
165
+ const traces = this.getTraces();
166
+ return {
167
+ entries: [
168
+ ...(options.legend ? traces.filter((trace) => trace.visible) : []),
169
+ ...(options.markerLegend
170
+ ? MarkerLegend.entries(traces, (id) => this.getMarker(id), options.markerLegend)
171
+ : []),
172
+ ],
173
+ textColor: this.theme.grid.textColor,
174
+ fontFamily: this.theme.fontFamily,
175
+ };
176
+ }
177
+ /** Export the mounted view as PNG, optionally with radial scales and a trace legend. */
178
+ async toPng(options = {}) {
179
+ this.assertAlive();
180
+ this.validateExportOptions(options);
181
+ const sources = [this.toSvg()];
182
+ if (options.scales) {
183
+ sources.push(options.scales.toSvg({ readout: options.scaleReadout }));
184
+ }
185
+ return ImageExporter.png(sources, options, this.exportLegend(options));
185
186
  }
186
187
  /** Remove this chart and release its event handlers. Safe to call more than once. */
187
188
  destroy() {
188
189
  if (this.destroyed) {
189
190
  return;
190
191
  }
191
- this.resizeObserver.disconnect();
192
192
  this.listeners.clear();
193
193
  this.clearTraces();
194
194
  this.destroyed = true;
195
- this.cursor.setMoveHandler(null);
196
- this.mouseGesture.destroy();
197
- this.zoomBehavior.on('start', null).on('zoom', null);
198
- this.svg.Element.interrupt().on('.zoom', null);
199
- this.svg.Element.selectAll('*').interrupt().on('.smithkit', null).on('.drag', null);
200
- this.svg.Element.remove();
201
- }
202
- updateViewportScale() {
203
- if (this.destroyed) {
204
- return;
205
- }
206
- const matrix = this.svg.Node.getScreenCTM();
207
- if (matrix) {
208
- const scale = Math.hypot(matrix.a, matrix.b);
209
- this.data.forEach((trace) => trace.setViewportScale(scale));
210
- }
211
- this.labelLayout.update();
195
+ this.renderer.destroy();
212
196
  }
213
197
  assertAlive() {
214
198
  if (this.destroyed) {
215
199
  throw new Error('This Smith chart has been destroyed. Create a new instance.');
216
200
  }
217
201
  }
218
- createScalers(size) {
219
- const impedance = new SmithScaler(d3.scaleLinear().domain([-1, 1]).range([0, size]), d3.scaleLinear().domain([1, -1]).range([0, size]), d3
220
- .scaleLinear()
221
- .domain([0, 1])
222
- .range([0, size / 2]));
223
- const admittance = new SmithScaler(d3.scaleLinear().domain([1, -1]).range([0, size]), d3.scaleLinear().domain([-1, 1]).range([0, size]), d3
224
- .scaleLinear()
225
- .domain([0, 1])
226
- .range([0, size / 2]));
227
- return { default: impedance, impedance, admittance };
228
- }
229
- cursorMove(p) {
230
- if (this.draggedMarkers.size > 0) {
231
- return;
232
- }
233
- this.cursor.Position = Complex.from(this.scalers.default.pointInvert(p));
234
- }
235
202
  markerDragChanged(marker, dragging) {
236
203
  const wasDragging = this.draggedMarkers.size > 0;
237
204
  if (dragging) {
@@ -244,109 +211,27 @@ export class Smith {
244
211
  if (isDragging === wasDragging) {
245
212
  return;
246
213
  }
247
- if (isDragging) {
248
- this.cursorBeforeMarkerDrag = this.svg.Node.style.getPropertyValue('cursor') || null;
249
- this.svg.Element.style('cursor', 'grabbing').style('--smithkit-marker-cursor', 'grabbing');
250
- this.cursor.hide();
251
- this.emit({ type: SmithEventType.Cursor, data: undefined });
252
- }
253
- else {
254
- this.svg.Element.style('cursor', () => this.cursorBeforeMarkerDrag).style('--smithkit-marker-cursor', null);
255
- this.cursorBeforeMarkerDrag = null;
256
- }
257
- }
258
- initCursor() {
259
- const cursor = new SmithCursor(this.scalers.default);
260
- cursor.Group.attr('class', 'smith-cursor');
261
- cursor.setMoveHandler(() => {
262
- this.emit({ type: SmithEventType.Cursor, data: this.cursorReading });
263
- });
264
- return cursor;
214
+ this.renderer.setMarkerDragging(isDragging);
265
215
  }
266
216
  /** Last cursor position. Use cursor events to detect pointer leave. */
267
217
  get cursorReading() {
268
- return RfCalculations.readReflection(this.cursor.Position, this.referenceImpedanceOhms);
269
- }
270
- initializeZoom() {
271
- const zoom = this.zoomBehavior
272
- .scaleExtent([0.6, 1000])
273
- .filter((event) => this.zoomEnabled &&
274
- this.draggedMarkers.size === 0 &&
275
- (!event.ctrlKey || event.type === 'wheel') &&
276
- !event.button)
277
- .on('start', (event) => {
278
- if (event.sourceEvent) {
279
- this.mouseGesture.capture(event.sourceEvent, 'zoom');
280
- }
281
- })
282
- .on('zoom', (event) => {
283
- if ((!this.zoomEnabled || this.draggedMarkers.size > 0) && !this.applyingView) {
284
- // An already active mouse/touch gesture still needs its normal end event.
285
- // Restore D3's view through its public API while that gesture finishes.
286
- this.applyView(this.transform);
287
- return;
288
- }
289
- this.onZoom(event.transform);
290
- });
291
- this.svg.Element.call(zoom);
292
- this.resetView();
218
+ return RfCalculations.readReflection(this.renderer.cursorPosition, this.referenceOhms);
293
219
  }
294
220
  /** Enable or disable wheel, double-click, and mouse/touch zoom/pan. Keeps the current view. */
295
221
  setZoomEnabled(enabled) {
296
222
  this.assertAlive();
297
- if (typeof enabled !== 'boolean') {
298
- throw new TypeError('Zoom enabled must be a boolean.');
299
- }
300
- if (this.zoomEnabled === enabled) {
301
- return;
302
- }
303
- this.zoomEnabled = enabled;
304
- if (!enabled) {
305
- this.svg.Element.interrupt();
306
- }
223
+ this.renderer.setZoomEnabled(enabled);
307
224
  }
308
- applyView(transform) {
309
- this.applyingView = true;
310
- try {
311
- this.svg.Element.call(this.zoomBehavior.transform, transform);
312
- }
313
- finally {
314
- this.applyingView = false;
315
- }
225
+ /** Enable or disable the cursor overlay and readings independently of zoom and markers. */
226
+ setCursorEnabled(enabled) {
227
+ this.assertAlive();
228
+ this.renderer.setCursorEnabled(enabled);
316
229
  }
317
230
  resetView() {
318
231
  this.assertAlive();
319
- const transform = this.defaultTransform;
320
- this.applyView(transform);
232
+ this.renderer.resetView();
321
233
  }
322
- onZoom(transform) {
323
- if (this.destroyed) {
324
- return;
325
- }
326
- this.transform = transform;
327
- this.container.Element.attr('transform', transform.toString());
328
- this.data.forEach((d) => d.zoom(transform));
329
- }
330
- cursorContainer() {
331
- const shape = this.drawReactanceAxis({ fill: 'transparent', stroke: 'none' });
332
- shape.Element.style('pointer-events', 'all')
333
- .on('pointermove.smithkit', (event) => {
334
- this.cursorMove(d3.pointer(event));
335
- })
336
- .on('pointerleave.smithkit', () => {
337
- this.cursor.hide();
338
- this.emit({ type: SmithEventType.Cursor, data: undefined });
339
- });
340
- return shape;
341
- }
342
- drawReactanceAxis(opts) {
343
- const c = this.calcs.resistanceCircle(0);
344
- c.p[0] = this.scalers.default.x(c.p[0]);
345
- c.p[1] = this.scalers.default.y(c.p[1]);
346
- c.r = this.scalers.default.r(c.r);
347
- return new SmithCircle(c, opts);
348
- }
349
- /** Add a named trace with one initial marker. Returns a chart-local, stable ID. */
234
+ /** Add a named trace without markers. Use addMarker to create markers explicitly. Returns a chart-local, stable ID. */
350
235
  addTrace(values, options = {}) {
351
236
  this.assertAlive();
352
237
  this.validateTraceOptions(options);
@@ -356,7 +241,6 @@ export class Smith {
356
241
  const data = this.createSmithData(values, this.nextDatasetColor, options);
357
242
  this.nextDatasetColor++;
358
243
  this.data.push(data);
359
- this.updateViewportScale();
360
244
  const id = this.traceMetadata.get(data).id;
361
245
  this.setTraceOptions(id, options);
362
246
  return id;
@@ -364,7 +248,8 @@ export class Smith {
364
248
  /** Detached metadata snapshots; IDs and marker display numbers survive removals. */
365
249
  getTraces() {
366
250
  return this.data.map((data) => ({
367
- ...this.traceMetadata.get(data),
251
+ id: this.traceMetadata.get(data).id,
252
+ name: this.traceMetadata.get(data).name,
368
253
  ...data.Style,
369
254
  color: data.Color,
370
255
  visible: data.Visible,
@@ -410,7 +295,7 @@ export class Smith {
410
295
  throw new TypeError('Trace name must not be empty.');
411
296
  }
412
297
  if (options.color !== undefined &&
413
- (typeof options.color !== 'string' || !d3.color(options.color))) {
298
+ (typeof options.color !== 'string' || !parseColor(options.color))) {
414
299
  throw new TypeError('Trace color must be a solid CSS color.');
415
300
  }
416
301
  if (options.visible !== undefined && typeof options.visible !== 'boolean') {
@@ -431,6 +316,7 @@ export class Smith {
431
316
  }
432
317
  if (options.color !== undefined) {
433
318
  data.setColor(options.color);
319
+ delete this.traceMetadata.get(data).colorIndex;
434
320
  }
435
321
  if (options.visible !== undefined) {
436
322
  data.setVisible(options.visible);
@@ -457,7 +343,7 @@ export class Smith {
457
343
  if (index < 0) {
458
344
  return false;
459
345
  }
460
- this.data[index].destroy();
346
+ this.renderer.removeTrace(this.data[index]);
461
347
  this.data.splice(index, 1);
462
348
  return true;
463
349
  }
@@ -537,17 +423,16 @@ export class Smith {
537
423
  this.data.forEach((data) => data.Markers.forEach((entry) => entry.marker.cancelDrag()));
538
424
  this.referenceOhms = referenceImpedanceOhms;
539
425
  this.data.forEach((data, index) => data.update(samples[index], 'sample-index'));
540
- this.cursor.hide();
541
- this.emit({ type: SmithEventType.Cursor, data: undefined });
426
+ this.renderer.hideCursor();
542
427
  }
543
428
  clearTraces() {
544
429
  this.assertAlive();
545
- this.data.forEach((dataset) => dataset.destroy());
430
+ this.data.forEach((dataset) => this.renderer.removeTrace(dataset));
546
431
  this.data = [];
547
432
  }
548
433
  createSmithData(values, dataset, options) {
549
- const color = d3.schemeCategory10[(1 + dataset) % d3.schemeCategory10.length];
550
- const data = new SmithData(values, color, this.transform, this.dataContainer, this.scalers.default, (marker, dragging) => {
434
+ const color = this.theme.traceColors[dataset % this.theme.traceColors.length];
435
+ const data = this.renderer.createTrace(values, color, options, (marker, dragging) => {
551
436
  this.markerDragChanged(marker, dragging);
552
437
  const snapshot = this.getMarker(this.markerId(marker));
553
438
  if (snapshot) {
@@ -556,9 +441,13 @@ export class Smith {
556
441
  data: snapshot,
557
442
  });
558
443
  }
559
- }, this.markerContainer, options);
444
+ });
560
445
  const number = this.nextTraceId++;
561
- this.traceMetadata.set(data, { id: `trace-${number}`, name: `Trace ${number}` });
446
+ this.traceMetadata.set(data, {
447
+ id: `trace-${number}`,
448
+ name: `Trace ${number}`,
449
+ colorIndex: dataset,
450
+ });
562
451
  data.setName(`Trace ${number}`);
563
452
  data.setMarkerSelectHandler((marker) => {
564
453
  const snapshot = this.getMarker(this.markerId(marker));
@@ -573,7 +462,6 @@ export class Smith {
573
462
  this.emit({ type: SmithEventType.Marker, data: snapshot });
574
463
  }
575
464
  });
576
- data.addMarker();
577
465
  return data;
578
466
  }
579
467
  /** Subscribe to chart events. The returned function removes this subscription. */
@@ -0,0 +1,6 @@
1
+ import type { SmithTheme } from './types.js';
2
+ /** Scope presentation tokens to one chart or scale component. Explicit SVG styles take priority. */
3
+ export declare class SvgTheme {
4
+ private constructor();
5
+ static apply(element: HTMLElement | SVGElement, theme: SmithTheme): void;
6
+ }
@@ -0,0 +1,21 @@
1
+ /** Scope presentation tokens to one chart or scale component. Explicit SVG styles take priority. */
2
+ export class SvgTheme {
3
+ constructor() { }
4
+ static apply(element, theme) {
5
+ for (const [section, value] of Object.entries(theme)) {
6
+ if (Array.isArray(value)) {
7
+ continue;
8
+ }
9
+ if (typeof value === 'object') {
10
+ for (const [key, entry] of Object.entries(value)) {
11
+ const suffix = key.toLowerCase().endsWith('fontsize') ? 'px' : '';
12
+ element.style.setProperty(`--smithkit-${section}-${key}`, String(entry) + suffix);
13
+ }
14
+ }
15
+ else {
16
+ element.style.setProperty(`--smithkit-${section}`, value);
17
+ }
18
+ }
19
+ element.style.backgroundColor = theme.background;
20
+ }
21
+ }
@@ -0,0 +1,8 @@
1
+ import type { SmithAppearance, SmithTheme } from './types.js';
2
+ /** Resolve and validate a detached theme before touching any chart state or DOM. */
3
+ export declare class Theme {
4
+ private constructor();
5
+ static resolve(appearance?: SmithAppearance): SmithTheme;
6
+ private static merge;
7
+ private static isColor;
8
+ }