smithkit 0.2.0 → 0.3.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/README.md +176 -6
- package/dist/lib/Smith.d.ts +13 -37
- package/dist/lib/Smith.js +82 -282
- package/dist/lib/appearance/SvgTheme.d.ts +6 -0
- package/dist/lib/appearance/SvgTheme.js +21 -0
- package/dist/lib/appearance/Theme.d.ts +8 -0
- package/dist/lib/appearance/Theme.js +120 -0
- package/dist/lib/appearance/types.d.ts +55 -0
- package/dist/lib/appearance/types.js +1 -0
- package/dist/lib/grid/ConstCircles.d.ts +0 -3
- package/dist/lib/grid/ConstCircles.js +6 -20
- package/dist/lib/grid/ConstQCircles.js +1 -1
- package/dist/lib/grid/ConstSwrCircles.js +1 -1
- package/dist/lib/grid/GridDefinitions.d.ts +7 -1
- package/dist/lib/grid/GridDefinitions.js +29 -0
- package/dist/lib/grid/GridLayerControl.d.ts +5 -4
- package/dist/lib/grid/GridLayerControl.js +11 -2
- package/dist/lib/grid/SmithGridLayer.d.ts +7 -1
- package/dist/lib/grid/SmithGridLayer.js +35 -5
- package/dist/lib/index.d.ts +3 -1
- package/dist/lib/interaction/SmithCursor.d.ts +0 -17
- package/dist/lib/interaction/SmithCursor.js +6 -14
- package/dist/lib/layers.d.ts +5 -1
- package/dist/lib/rendering/SvgChartRenderer.d.ts +66 -0
- package/dist/lib/rendering/SvgChartRenderer.js +342 -0
- package/dist/lib/scales/SmithPeripheralScales.js +11 -11
- package/dist/lib/scales/SmithScales.d.ts +10 -3
- package/dist/lib/scales/SmithScales.js +73 -39
- package/dist/lib/svg/ImageExporter.d.ts +18 -0
- package/dist/lib/svg/ImageExporter.js +211 -0
- package/dist/lib/svg/MarkerLegend.d.ts +12 -0
- package/dist/lib/svg/MarkerLegend.js +63 -0
- package/dist/lib/svg/SvgExporter.d.ts +6 -1
- package/dist/lib/svg/SvgExporter.js +50 -5
- package/dist/lib/svg/export.d.ts +36 -0
- package/dist/lib/svg/export.js +1 -0
- package/dist/lib/traces/SmithMarker.js +8 -5
- package/package.json +1 -1
package/dist/lib/Smith.js
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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 { MarkerLegend } from './svg/MarkerLegend.js';
|
|
2
|
+
import { ImageExporter } from './svg/ImageExporter.js';
|
|
3
|
+
import { Theme } from './appearance/Theme.js';
|
|
4
|
+
import { color as parseColor } from 'd3';
|
|
5
|
+
import { SvgChartRenderer } from './rendering/SvgChartRenderer.js';
|
|
19
6
|
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
7
|
import { RfCalculations } from './rf/RfCalculations.js';
|
|
24
8
|
import { MarkerMeasurements } from './MarkerMeasurements.js';
|
|
25
9
|
export var SmithEventType;
|
|
@@ -32,206 +16,101 @@ export var SmithEventType;
|
|
|
32
16
|
})(SmithEventType || (SmithEventType = {}));
|
|
33
17
|
export class Smith {
|
|
34
18
|
referenceOhms;
|
|
35
|
-
|
|
36
|
-
|
|
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();
|
|
19
|
+
theme;
|
|
20
|
+
renderer;
|
|
57
21
|
data = [];
|
|
58
22
|
destroyed = false;
|
|
59
|
-
mouseGesture = new MouseGesture();
|
|
60
23
|
nextDatasetColor = 0;
|
|
61
24
|
nextTraceId = 1;
|
|
62
25
|
nextMarkerId = 1;
|
|
63
26
|
traceMetadata = new WeakMap();
|
|
64
27
|
markerIds = new WeakMap();
|
|
65
28
|
draggedMarkers = new Set();
|
|
66
|
-
cursorBeforeMarkerDrag = null;
|
|
67
29
|
listeners = new Set();
|
|
68
30
|
layers;
|
|
69
31
|
peripheralScales;
|
|
70
|
-
constructor(referenceOhms = 50) {
|
|
32
|
+
constructor(referenceOhms = 50, appearance = {}) {
|
|
71
33
|
this.referenceOhms = referenceOhms;
|
|
72
34
|
if (!Number.isFinite(referenceOhms) || referenceOhms <= 0) {
|
|
73
35
|
throw new RangeError('Reference impedance must be positive and finite.');
|
|
74
36
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
data: gridData,
|
|
82
|
-
scaler: this.scalers.default,
|
|
83
|
-
showMinor: true,
|
|
37
|
+
this.theme = Theme.resolve(appearance);
|
|
38
|
+
this.renderer = new SvgChartRenderer((position) => {
|
|
39
|
+
this.emit({
|
|
40
|
+
type: SmithEventType.Cursor,
|
|
41
|
+
data: position ? RfCalculations.readReflection(position, this.referenceOhms) : undefined,
|
|
42
|
+
});
|
|
84
43
|
});
|
|
85
|
-
this.
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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));
|
|
44
|
+
this.renderer.setTheme(this.theme);
|
|
45
|
+
this.layers = this.renderer.layers;
|
|
46
|
+
this.peripheralScales = this.renderer.peripheralScales;
|
|
47
|
+
}
|
|
48
|
+
/** Replace the appearance while retaining explicit layer styles and trace colors. */
|
|
49
|
+
setAppearance(appearance) {
|
|
50
|
+
this.assertAlive();
|
|
51
|
+
const theme = Theme.resolve(appearance);
|
|
52
|
+
this.theme = theme;
|
|
53
|
+
this.renderer.setTheme(theme);
|
|
54
|
+
for (const data of this.data) {
|
|
55
|
+
const index = this.traceMetadata.get(data).colorIndex;
|
|
56
|
+
if (index !== undefined) {
|
|
57
|
+
data.setColor(theme.traceColors[index % theme.traceColors.length]);
|
|
58
|
+
}
|
|
138
59
|
}
|
|
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();
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
this.peripheralScaleRenderer.hide();
|
|
160
|
-
}
|
|
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);
|
|
171
60
|
}
|
|
172
61
|
draw(target) {
|
|
173
62
|
this.assertAlive();
|
|
174
|
-
|
|
175
|
-
if (!host) {
|
|
176
|
-
throw new Error('Chart container was not found.');
|
|
177
|
-
}
|
|
178
|
-
host.appendChild(this.svg.Node);
|
|
179
|
-
this.updateViewportScale();
|
|
63
|
+
this.renderer.draw(target);
|
|
180
64
|
}
|
|
181
|
-
/** Export the current mounted view as standalone SVG with
|
|
182
|
-
toSvg() {
|
|
65
|
+
/** Export the current mounted view as standalone SVG with its configured background. */
|
|
66
|
+
toSvg(options) {
|
|
183
67
|
this.assertAlive();
|
|
184
|
-
|
|
68
|
+
const source = this.renderer.toSvg();
|
|
69
|
+
if (options === undefined) {
|
|
70
|
+
return source;
|
|
71
|
+
}
|
|
72
|
+
const sources = [source];
|
|
73
|
+
if (options.scales) {
|
|
74
|
+
sources.push(options.scales.toSvg({ readout: options.scaleReadout }));
|
|
75
|
+
}
|
|
76
|
+
return ImageExporter.svg(sources, options, this.exportLegend(options));
|
|
77
|
+
}
|
|
78
|
+
exportLegend(options) {
|
|
79
|
+
const traces = this.getTraces();
|
|
80
|
+
return {
|
|
81
|
+
entries: [
|
|
82
|
+
...(options.legend ? traces.filter((trace) => trace.visible) : []),
|
|
83
|
+
...(options.markerLegend
|
|
84
|
+
? MarkerLegend.entries(traces, (id) => this.getMarker(id), options.markerLegend)
|
|
85
|
+
: []),
|
|
86
|
+
],
|
|
87
|
+
textColor: this.theme.grid.textColor,
|
|
88
|
+
fontFamily: this.theme.fontFamily,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Export the mounted view as PNG, optionally with radial scales and a trace legend. */
|
|
92
|
+
async toPng(options = {}) {
|
|
93
|
+
const sources = [this.toSvg()];
|
|
94
|
+
if (options.scales) {
|
|
95
|
+
sources.push(options.scales.toSvg({ readout: options.scaleReadout }));
|
|
96
|
+
}
|
|
97
|
+
return ImageExporter.png(sources, options, this.exportLegend(options));
|
|
185
98
|
}
|
|
186
99
|
/** Remove this chart and release its event handlers. Safe to call more than once. */
|
|
187
100
|
destroy() {
|
|
188
101
|
if (this.destroyed) {
|
|
189
102
|
return;
|
|
190
103
|
}
|
|
191
|
-
this.resizeObserver.disconnect();
|
|
192
104
|
this.listeners.clear();
|
|
193
105
|
this.clearTraces();
|
|
194
106
|
this.destroyed = true;
|
|
195
|
-
this.
|
|
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();
|
|
107
|
+
this.renderer.destroy();
|
|
212
108
|
}
|
|
213
109
|
assertAlive() {
|
|
214
110
|
if (this.destroyed) {
|
|
215
111
|
throw new Error('This Smith chart has been destroyed. Create a new instance.');
|
|
216
112
|
}
|
|
217
113
|
}
|
|
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
114
|
markerDragChanged(marker, dragging) {
|
|
236
115
|
const wasDragging = this.draggedMarkers.size > 0;
|
|
237
116
|
if (dragging) {
|
|
@@ -244,107 +123,23 @@ export class Smith {
|
|
|
244
123
|
if (isDragging === wasDragging) {
|
|
245
124
|
return;
|
|
246
125
|
}
|
|
126
|
+
this.renderer.setMarkerDragging(isDragging);
|
|
247
127
|
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
128
|
this.emit({ type: SmithEventType.Cursor, data: undefined });
|
|
252
129
|
}
|
|
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;
|
|
265
130
|
}
|
|
266
131
|
/** Last cursor position. Use cursor events to detect pointer leave. */
|
|
267
132
|
get cursorReading() {
|
|
268
|
-
return RfCalculations.readReflection(this.
|
|
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();
|
|
133
|
+
return RfCalculations.readReflection(this.renderer.cursorPosition, this.referenceOhms);
|
|
293
134
|
}
|
|
294
135
|
/** Enable or disable wheel, double-click, and mouse/touch zoom/pan. Keeps the current view. */
|
|
295
136
|
setZoomEnabled(enabled) {
|
|
296
137
|
this.assertAlive();
|
|
297
|
-
|
|
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
|
-
}
|
|
307
|
-
}
|
|
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
|
-
}
|
|
138
|
+
this.renderer.setZoomEnabled(enabled);
|
|
316
139
|
}
|
|
317
140
|
resetView() {
|
|
318
141
|
this.assertAlive();
|
|
319
|
-
|
|
320
|
-
this.applyView(transform);
|
|
321
|
-
}
|
|
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);
|
|
142
|
+
this.renderer.resetView();
|
|
348
143
|
}
|
|
349
144
|
/** Add a named trace with one initial marker. Returns a chart-local, stable ID. */
|
|
350
145
|
addTrace(values, options = {}) {
|
|
@@ -356,7 +151,6 @@ export class Smith {
|
|
|
356
151
|
const data = this.createSmithData(values, this.nextDatasetColor, options);
|
|
357
152
|
this.nextDatasetColor++;
|
|
358
153
|
this.data.push(data);
|
|
359
|
-
this.updateViewportScale();
|
|
360
154
|
const id = this.traceMetadata.get(data).id;
|
|
361
155
|
this.setTraceOptions(id, options);
|
|
362
156
|
return id;
|
|
@@ -364,7 +158,8 @@ export class Smith {
|
|
|
364
158
|
/** Detached metadata snapshots; IDs and marker display numbers survive removals. */
|
|
365
159
|
getTraces() {
|
|
366
160
|
return this.data.map((data) => ({
|
|
367
|
-
|
|
161
|
+
id: this.traceMetadata.get(data).id,
|
|
162
|
+
name: this.traceMetadata.get(data).name,
|
|
368
163
|
...data.Style,
|
|
369
164
|
color: data.Color,
|
|
370
165
|
visible: data.Visible,
|
|
@@ -410,7 +205,7 @@ export class Smith {
|
|
|
410
205
|
throw new TypeError('Trace name must not be empty.');
|
|
411
206
|
}
|
|
412
207
|
if (options.color !== undefined &&
|
|
413
|
-
(typeof options.color !== 'string' || !
|
|
208
|
+
(typeof options.color !== 'string' || !parseColor(options.color))) {
|
|
414
209
|
throw new TypeError('Trace color must be a solid CSS color.');
|
|
415
210
|
}
|
|
416
211
|
if (options.visible !== undefined && typeof options.visible !== 'boolean') {
|
|
@@ -431,6 +226,7 @@ export class Smith {
|
|
|
431
226
|
}
|
|
432
227
|
if (options.color !== undefined) {
|
|
433
228
|
data.setColor(options.color);
|
|
229
|
+
delete this.traceMetadata.get(data).colorIndex;
|
|
434
230
|
}
|
|
435
231
|
if (options.visible !== undefined) {
|
|
436
232
|
data.setVisible(options.visible);
|
|
@@ -457,7 +253,7 @@ export class Smith {
|
|
|
457
253
|
if (index < 0) {
|
|
458
254
|
return false;
|
|
459
255
|
}
|
|
460
|
-
this.data[index]
|
|
256
|
+
this.renderer.removeTrace(this.data[index]);
|
|
461
257
|
this.data.splice(index, 1);
|
|
462
258
|
return true;
|
|
463
259
|
}
|
|
@@ -537,17 +333,17 @@ export class Smith {
|
|
|
537
333
|
this.data.forEach((data) => data.Markers.forEach((entry) => entry.marker.cancelDrag()));
|
|
538
334
|
this.referenceOhms = referenceImpedanceOhms;
|
|
539
335
|
this.data.forEach((data, index) => data.update(samples[index], 'sample-index'));
|
|
540
|
-
this.
|
|
336
|
+
this.renderer.hideCursor();
|
|
541
337
|
this.emit({ type: SmithEventType.Cursor, data: undefined });
|
|
542
338
|
}
|
|
543
339
|
clearTraces() {
|
|
544
340
|
this.assertAlive();
|
|
545
|
-
this.data.forEach((dataset) =>
|
|
341
|
+
this.data.forEach((dataset) => this.renderer.removeTrace(dataset));
|
|
546
342
|
this.data = [];
|
|
547
343
|
}
|
|
548
344
|
createSmithData(values, dataset, options) {
|
|
549
|
-
const color =
|
|
550
|
-
const data =
|
|
345
|
+
const color = this.theme.traceColors[dataset % this.theme.traceColors.length];
|
|
346
|
+
const data = this.renderer.createTrace(values, color, options, (marker, dragging) => {
|
|
551
347
|
this.markerDragChanged(marker, dragging);
|
|
552
348
|
const snapshot = this.getMarker(this.markerId(marker));
|
|
553
349
|
if (snapshot) {
|
|
@@ -556,9 +352,13 @@ export class Smith {
|
|
|
556
352
|
data: snapshot,
|
|
557
353
|
});
|
|
558
354
|
}
|
|
559
|
-
}
|
|
355
|
+
});
|
|
560
356
|
const number = this.nextTraceId++;
|
|
561
|
-
this.traceMetadata.set(data, {
|
|
357
|
+
this.traceMetadata.set(data, {
|
|
358
|
+
id: `trace-${number}`,
|
|
359
|
+
name: `Trace ${number}`,
|
|
360
|
+
colorIndex: dataset,
|
|
361
|
+
});
|
|
562
362
|
data.setName(`Trace ${number}`);
|
|
563
363
|
data.setMarkerSelectHandler((marker) => {
|
|
564
364
|
const snapshot = this.getMarker(this.markerId(marker));
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { color } from 'd3';
|
|
2
|
+
/** Resolve and validate a detached theme before touching any chart state or DOM. */
|
|
3
|
+
export class Theme {
|
|
4
|
+
constructor() { }
|
|
5
|
+
static resolve(appearance = {}) {
|
|
6
|
+
if (!appearance || typeof appearance !== 'object' || Array.isArray(appearance)) {
|
|
7
|
+
throw new TypeError('Appearance must be an object.');
|
|
8
|
+
}
|
|
9
|
+
const name = appearance.theme === undefined ? 'light' : appearance.theme;
|
|
10
|
+
if (name !== 'light' && name !== 'dark') {
|
|
11
|
+
throw new TypeError('Theme must be light or dark.');
|
|
12
|
+
}
|
|
13
|
+
const dark = name === 'dark';
|
|
14
|
+
const theme = {
|
|
15
|
+
background: dark ? '#0f172a' : 'transparent',
|
|
16
|
+
fontFamily: 'Verdana, sans-serif',
|
|
17
|
+
grid: {
|
|
18
|
+
stroke: dark ? '#94a3b8' : '#64748b',
|
|
19
|
+
textColor: dark ? '#e2e8f0' : '#111827',
|
|
20
|
+
majorWidth: 1,
|
|
21
|
+
minorWidth: 0.6,
|
|
22
|
+
fontSize: 7,
|
|
23
|
+
},
|
|
24
|
+
boundary: { stroke: dark ? '#cbd5e1' : '#334155', width: 1 },
|
|
25
|
+
circles: { q: dark ? '#93c5fd' : 'blue', vswr: dark ? '#fbbf24' : 'orange', width: 1 },
|
|
26
|
+
scales: {
|
|
27
|
+
stroke: dark ? '#94a3b8' : '#64748b',
|
|
28
|
+
boundaryColor: dark ? '#cbd5e1' : '#334155',
|
|
29
|
+
textColor: dark ? '#e2e8f0' : '#334155',
|
|
30
|
+
valueColor: dark ? '#fca5a5' : '#b42318',
|
|
31
|
+
indicatorColor: dark ? '#f87171' : '#dc2626',
|
|
32
|
+
indicatorOutline: dark ? '#0f172a' : 'white',
|
|
33
|
+
fontSize: 10,
|
|
34
|
+
titleFontSize: 12,
|
|
35
|
+
valueFontSize: 11,
|
|
36
|
+
peripheralFontSize: 10.5,
|
|
37
|
+
},
|
|
38
|
+
cursor: {
|
|
39
|
+
pointColor: dark ? '#fca5a5' : 'red',
|
|
40
|
+
impedanceColor: dark ? '#fca5a5' : 'red',
|
|
41
|
+
admittanceColor: dark ? '#86efac' : 'green',
|
|
42
|
+
pointSize: 5,
|
|
43
|
+
lineWidth: 1,
|
|
44
|
+
},
|
|
45
|
+
marker: {
|
|
46
|
+
outlineColor: dark ? '#e2e8f0' : 'gray',
|
|
47
|
+
textColor: 'white',
|
|
48
|
+
focusColor: dark ? '#93c5fd' : '#1d4ed8',
|
|
49
|
+
focusHaloColor: dark ? '#0f172a' : 'white',
|
|
50
|
+
},
|
|
51
|
+
traceColors: dark
|
|
52
|
+
? [
|
|
53
|
+
'#fb923c',
|
|
54
|
+
'#4ade80',
|
|
55
|
+
'#f87171',
|
|
56
|
+
'#c084fc',
|
|
57
|
+
'#d6a681',
|
|
58
|
+
'#f472b6',
|
|
59
|
+
'#cbd5e1',
|
|
60
|
+
'#d4d45c',
|
|
61
|
+
'#22d3ee',
|
|
62
|
+
'#60a5fa',
|
|
63
|
+
]
|
|
64
|
+
: [
|
|
65
|
+
'#ff7f0e',
|
|
66
|
+
'#2ca02c',
|
|
67
|
+
'#d62728',
|
|
68
|
+
'#9467bd',
|
|
69
|
+
'#8c564b',
|
|
70
|
+
'#e377c2',
|
|
71
|
+
'#7f7f7f',
|
|
72
|
+
'#bcbd22',
|
|
73
|
+
'#17becf',
|
|
74
|
+
'#1f77b4',
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
if (appearance.overrides !== undefined) {
|
|
78
|
+
Theme.merge(theme, appearance.overrides);
|
|
79
|
+
}
|
|
80
|
+
return theme;
|
|
81
|
+
}
|
|
82
|
+
static merge(target, source, path = 'appearance') {
|
|
83
|
+
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
|
84
|
+
throw new TypeError(`${path} overrides must be an object.`);
|
|
85
|
+
}
|
|
86
|
+
for (const [key, value] of Object.entries(source)) {
|
|
87
|
+
if (!Object.hasOwn(target, key)) {
|
|
88
|
+
throw new TypeError(`Unknown appearance setting: ${path}.${key}.`);
|
|
89
|
+
}
|
|
90
|
+
if (value === undefined) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const previous = target[key];
|
|
94
|
+
if (Array.isArray(previous)) {
|
|
95
|
+
if (!Array.isArray(value) || value.length === 0 || !value.every(Theme.isColor)) {
|
|
96
|
+
throw new TypeError('Trace colors must be a non-empty array of solid CSS colors.');
|
|
97
|
+
}
|
|
98
|
+
target[key] = [...value];
|
|
99
|
+
}
|
|
100
|
+
else if (typeof previous === 'object') {
|
|
101
|
+
Theme.merge(previous, value, `${path}.${key}`);
|
|
102
|
+
}
|
|
103
|
+
else if (typeof previous === 'number') {
|
|
104
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
105
|
+
throw new RangeError(`${path}.${key} must be positive and finite.`);
|
|
106
|
+
}
|
|
107
|
+
target[key] = value;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
if (key === 'fontFamily' ? typeof value !== 'string' || !value.trim() : !Theme.isColor(value)) {
|
|
111
|
+
throw new TypeError(`${path}.${key} must be ${key === 'fontFamily' ? 'a non-empty font family' : 'a solid CSS color'}.`);
|
|
112
|
+
}
|
|
113
|
+
target[key] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
static isColor(value) {
|
|
118
|
+
return typeof value === 'string' && color(value) !== null;
|
|
119
|
+
}
|
|
120
|
+
}
|