smithkit 0.1.1 → 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.
Files changed (146) hide show
  1. package/README.md +291 -42
  2. package/dist/lib/MarkerMeasurements.d.ts +6 -0
  3. package/dist/lib/MarkerMeasurements.js +22 -0
  4. package/dist/lib/Smith.d.ts +18 -35
  5. package/dist/lib/Smith.js +107 -275
  6. package/dist/lib/SmithFormatter.d.ts +10 -0
  7. package/dist/lib/SmithFormatter.js +19 -0
  8. package/dist/lib/appearance/SvgTheme.d.ts +6 -0
  9. package/dist/lib/appearance/SvgTheme.js +21 -0
  10. package/dist/lib/appearance/Theme.d.ts +8 -0
  11. package/dist/lib/appearance/Theme.js +120 -0
  12. package/dist/lib/appearance/types.d.ts +55 -0
  13. package/dist/lib/grid/CircleLayerControl.d.ts +15 -0
  14. package/dist/lib/grid/CircleLayerControl.js +40 -0
  15. package/dist/lib/{draw → grid}/ConstCircles.d.ts +4 -9
  16. package/dist/lib/{draw → grid}/ConstCircles.js +8 -22
  17. package/dist/lib/{draw → grid}/ConstQCircles.d.ts +2 -2
  18. package/dist/lib/{draw → grid}/ConstQCircles.js +4 -4
  19. package/dist/lib/{draw → grid}/ConstSwrCircles.d.ts +2 -2
  20. package/dist/lib/{draw → grid}/ConstSwrCircles.js +4 -4
  21. package/dist/lib/grid/GridDefinitions.d.ts +18 -0
  22. package/dist/lib/grid/GridDefinitions.js +167 -0
  23. package/dist/lib/grid/GridGeometry.d.ts +26 -0
  24. package/dist/lib/grid/GridGeometry.js +58 -0
  25. package/dist/lib/grid/GridLabels.d.ts +8 -0
  26. package/dist/lib/grid/GridLabels.js +45 -0
  27. package/dist/lib/grid/GridLayerControl.d.ts +13 -0
  28. package/dist/lib/grid/GridLayerControl.js +48 -0
  29. package/dist/lib/grid/LayerControl.d.ts +6 -0
  30. package/dist/lib/grid/LayerControl.js +16 -0
  31. package/dist/lib/grid/SmithGridLayer.d.ts +26 -0
  32. package/dist/lib/grid/SmithGridLayer.js +95 -0
  33. package/dist/lib/grid/types.d.ts +1 -1
  34. package/dist/lib/index.d.ts +11 -11
  35. package/dist/lib/index.js +5 -7
  36. package/dist/lib/{draw → interaction}/SmithCursor.d.ts +3 -20
  37. package/dist/lib/{draw → interaction}/SmithCursor.js +12 -20
  38. package/dist/lib/io/Touchstone.d.ts +10 -0
  39. package/dist/lib/io/Touchstone.js +75 -0
  40. package/dist/lib/layers.d.ts +7 -9
  41. package/dist/lib/layers.js +1 -73
  42. package/dist/lib/{complex → math}/Complex.d.ts +1 -0
  43. package/dist/lib/{complex → math}/Complex.js +1 -1
  44. package/dist/lib/math/geometry.d.ts +14 -0
  45. package/dist/lib/measurements.d.ts +2 -4
  46. package/dist/lib/measurements.js +1 -19
  47. package/dist/lib/rendering/SvgChartRenderer.d.ts +66 -0
  48. package/dist/lib/rendering/SvgChartRenderer.js +342 -0
  49. package/dist/lib/rf/RfCalculations.d.ts +65 -0
  50. package/dist/lib/rf/RfCalculations.js +161 -0
  51. package/dist/lib/{SmithConstantCircle.d.ts → rf/SmithConstantCircle.d.ts} +2 -3
  52. package/dist/lib/{SmithConstantCircle.js → rf/SmithConstantCircle.js} +3 -4
  53. package/dist/lib/scales/PeripheralScaleDefinitions.d.ts +16 -0
  54. package/dist/lib/scales/PeripheralScaleDefinitions.js +54 -0
  55. package/dist/lib/scales/RadialScaleDefinitions.d.ts +15 -0
  56. package/dist/lib/scales/RadialScaleDefinitions.js +112 -0
  57. package/dist/lib/scales/SmithPeripheralScales.d.ts +4 -2
  58. package/dist/lib/scales/SmithPeripheralScales.js +55 -35
  59. package/dist/lib/scales/SmithScales.d.ts +11 -2
  60. package/dist/lib/scales/SmithScales.js +78 -38
  61. package/dist/lib/svg/ImageExporter.d.ts +18 -0
  62. package/dist/lib/svg/ImageExporter.js +211 -0
  63. package/dist/lib/svg/LabelLayout.d.ts +17 -0
  64. package/dist/lib/svg/LabelLayout.js +145 -0
  65. package/dist/lib/svg/MarkerLegend.d.ts +12 -0
  66. package/dist/lib/svg/MarkerLegend.js +63 -0
  67. package/dist/lib/{draw → svg}/SmithArc.d.ts +1 -1
  68. package/dist/lib/{draw → svg}/SmithCircle.d.ts +1 -1
  69. package/dist/lib/{draw → svg}/SmithLine.d.ts +1 -1
  70. package/dist/lib/{draw → svg}/SmithScaler.d.ts +1 -4
  71. package/dist/lib/{draw → svg}/SmithText.d.ts +1 -1
  72. package/dist/lib/svg/SvgExporter.d.ts +19 -0
  73. package/dist/lib/svg/SvgExporter.js +182 -0
  74. package/dist/lib/svg/export.d.ts +36 -0
  75. package/dist/lib/{draw → traces}/SmithData.d.ts +24 -35
  76. package/dist/lib/traces/SmithData.js +195 -0
  77. package/dist/lib/{draw → traces}/SmithMarker.d.ts +14 -3
  78. package/dist/lib/traces/SmithMarker.js +240 -0
  79. package/dist/lib/traces/TraceModel.d.ts +31 -0
  80. package/dist/lib/traces/TraceModel.js +123 -0
  81. package/dist/lib/traces/TraceRenderer.d.ts +33 -0
  82. package/dist/lib/traces/TraceRenderer.js +152 -0
  83. package/package.json +4 -1
  84. package/dist/lib/SmithArcsDefs.d.ts +0 -10
  85. package/dist/lib/SmithArcsDefs.js +0 -10
  86. package/dist/lib/components.d.ts +0 -14
  87. package/dist/lib/components.js +0 -26
  88. package/dist/lib/draw/SmithData.js +0 -366
  89. package/dist/lib/draw/SmithGridLayer.d.ts +0 -19
  90. package/dist/lib/draw/SmithGridLayer.js +0 -48
  91. package/dist/lib/draw/SmithMarker.js +0 -122
  92. package/dist/lib/formatting.d.ts +0 -7
  93. package/dist/lib/formatting.js +0 -16
  94. package/dist/lib/grid/definitions.d.ts +0 -3
  95. package/dist/lib/grid/definitions.js +0 -123
  96. package/dist/lib/grid/geometry.d.ts +0 -20
  97. package/dist/lib/grid/geometry.js +0 -55
  98. package/dist/lib/grid/labels.d.ts +0 -3
  99. package/dist/lib/grid/labels.js +0 -41
  100. package/dist/lib/io/touchstone.d.ts +0 -7
  101. package/dist/lib/io/touchstone.js +0 -72
  102. package/dist/lib/math/complexRatio.d.ts +0 -3
  103. package/dist/lib/math/complexRatio.js +0 -30
  104. package/dist/lib/renormalization.d.ts +0 -10
  105. package/dist/lib/renormalization.js +0 -62
  106. package/dist/lib/rf.d.ts +0 -35
  107. package/dist/lib/rf.js +0 -74
  108. package/dist/lib/scales/peripheralScales.d.ts +0 -12
  109. package/dist/lib/scales/peripheralScales.js +0 -49
  110. package/dist/lib/scales/radialScales.d.ts +0 -12
  111. package/dist/lib/scales/radialScales.js +0 -109
  112. package/dist/lib/shapes/Arc.d.ts +0 -6
  113. package/dist/lib/shapes/Circle.d.ts +0 -5
  114. package/dist/lib/shapes/Line.d.ts +0 -5
  115. package/dist/lib/shapes/Point.d.ts +0 -1
  116. package/dist/lib/shapes/Point.js +0 -1
  117. /package/dist/lib/{draw/SmithDrawOptions.js → appearance/types.js} +0 -0
  118. /package/dist/lib/{draw → grid}/ConstConductance.d.ts +0 -0
  119. /package/dist/lib/{draw → grid}/ConstConductance.js +0 -0
  120. /package/dist/lib/{draw → grid}/ConstReactance.d.ts +0 -0
  121. /package/dist/lib/{draw → grid}/ConstReactance.js +0 -0
  122. /package/dist/lib/{draw → grid}/ConstResistance.d.ts +0 -0
  123. /package/dist/lib/{draw → grid}/ConstResistance.js +0 -0
  124. /package/dist/lib/{draw → grid}/ConstSusceptance.d.ts +0 -0
  125. /package/dist/lib/{draw → grid}/ConstSusceptance.js +0 -0
  126. /package/dist/lib/{arcs → grid}/Tick.d.ts +0 -0
  127. /package/dist/lib/{arcs → grid}/Tick.js +0 -0
  128. /package/dist/lib/{draw → interaction}/MouseGesture.d.ts +0 -0
  129. /package/dist/lib/{draw → interaction}/MouseGesture.js +0 -0
  130. /package/dist/lib/{shapes/Arc.js → math/geometry.js} +0 -0
  131. /package/dist/lib/{draw → svg}/SmithArc.js +0 -0
  132. /package/dist/lib/{draw → svg}/SmithCircle.js +0 -0
  133. /package/dist/lib/{draw → svg}/SmithDrawOptions.d.ts +0 -0
  134. /package/dist/lib/{shapes/Circle.js → svg/SmithDrawOptions.js} +0 -0
  135. /package/dist/lib/{draw → svg}/SmithGroup.d.ts +0 -0
  136. /package/dist/lib/{draw → svg}/SmithGroup.js +0 -0
  137. /package/dist/lib/{draw → svg}/SmithLine.js +0 -0
  138. /package/dist/lib/{draw → svg}/SmithPath.d.ts +0 -0
  139. /package/dist/lib/{draw → svg}/SmithPath.js +0 -0
  140. /package/dist/lib/{draw → svg}/SmithScaler.js +0 -0
  141. /package/dist/lib/{draw → svg}/SmithShape.d.ts +0 -0
  142. /package/dist/lib/{draw → svg}/SmithShape.js +0 -0
  143. /package/dist/lib/{draw → svg}/SmithSvg.d.ts +0 -0
  144. /package/dist/lib/{draw → svg}/SmithSvg.js +0 -0
  145. /package/dist/lib/{draw → svg}/SmithText.js +0 -0
  146. /package/dist/lib/{shapes/Line.js → svg/export.js} +0 -0
package/dist/lib/Smith.js CHANGED
@@ -1,224 +1,116 @@
1
- import * as d3 from 'd3';
2
- import { MouseGesture } from './draw/MouseGesture.js';
3
- import { SmithSvg } from './draw/SmithSvg.js';
4
- import { SmithGroup } from './draw/SmithGroup.js';
5
- import { SmithCircle } from './draw/SmithCircle.js';
6
- import { SmithData } from './draw/SmithData.js';
7
- import { SmithCursor } from './draw/SmithCursor.js';
8
- import { ConstResistance } from './draw/ConstResistance.js';
9
- import { ConstReactance } from './draw/ConstReactance.js';
10
- import { ConstConductance } from './draw/ConstConductance.js';
11
- import { ConstSusceptance } from './draw/ConstSusceptance.js';
12
- import { ConstQCircles } from './draw/ConstQCircles.js';
13
- import { ConstSwrCircles } from './draw/ConstSwrCircles.js';
14
- import { SmithScaler } from './draw/SmithScaler.js';
15
- import { SmithConstantCircle } from './SmithConstantCircle.js';
16
- import { SmithArcsDefs } from './SmithArcsDefs.js';
17
- import { Complex } from './complex/Complex.js';
18
- import { SmithPeripheralScales } from './scales/SmithPeripheralScales.js';
19
- import { gridLayer, circleLayer } from './layers.js';
20
- import { renormalizeSamples } from './renormalization.js';
21
- import { readReflection } from './rf.js';
22
- import { compareMarkerReadings } from './measurements.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';
6
+ import { Complex } from './math/Complex.js';
7
+ import { RfCalculations } from './rf/RfCalculations.js';
8
+ import { MarkerMeasurements } from './MarkerMeasurements.js';
23
9
  export var SmithEventType;
24
10
  (function (SmithEventType) {
25
11
  SmithEventType["Cursor"] = "cursor";
26
12
  SmithEventType["Marker"] = "marker";
13
+ SmithEventType["MarkerSelect"] = "marker-select";
27
14
  SmithEventType["MarkerDragStart"] = "marker-drag-start";
28
15
  SmithEventType["MarkerDragEnd"] = "marker-drag-end";
29
16
  })(SmithEventType || (SmithEventType = {}));
30
17
  export class Smith {
31
18
  referenceOhms;
32
- calcs = new SmithConstantCircle();
33
- scalers;
34
- transform = d3.zoomIdentity;
35
- zoomEnabled = true;
36
- applyingView = false;
37
- zoomBehavior = d3.zoom();
38
- svg;
39
- resizeObserver;
40
- container;
41
- dataContainer;
42
- markerContainer;
43
- reactanceAxis;
44
- constResistance;
45
- constReactance;
46
- constConductance;
47
- constSusceptance;
48
- constSwrCircles;
49
- constQCircles;
50
- cursor;
51
- peripheralScaleRenderer = new SmithPeripheralScales();
19
+ theme;
20
+ renderer;
52
21
  data = [];
53
22
  destroyed = false;
54
- mouseGesture = new MouseGesture();
55
23
  nextDatasetColor = 0;
56
24
  nextTraceId = 1;
57
25
  nextMarkerId = 1;
58
26
  traceMetadata = new WeakMap();
59
27
  markerIds = new WeakMap();
60
28
  draggedMarkers = new Set();
61
- cursorBeforeMarkerDrag = null;
62
29
  listeners = new Set();
63
30
  layers;
64
31
  peripheralScales;
65
- constructor(referenceOhms = 50) {
32
+ constructor(referenceOhms = 50, appearance = {}) {
66
33
  this.referenceOhms = referenceOhms;
67
34
  if (!Number.isFinite(referenceOhms) || referenceOhms <= 0) {
68
35
  throw new RangeError('Reference impedance must be positive and finite.');
69
36
  }
70
- const viewBoxSize = 500;
71
- const gridData = SmithArcsDefs.getData();
72
- this.scalers = this.createScalers(viewBoxSize);
73
- this.svg = new SmithSvg(viewBoxSize);
74
- this.container = new SmithGroup();
75
- this.constResistance = new ConstResistance({
76
- data: gridData,
77
- scaler: this.scalers.default,
78
- 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
+ });
79
43
  });
80
- this.constResistance.show();
81
- this.constReactance = new ConstReactance({
82
- data: gridData,
83
- scaler: this.scalers.default,
84
- showMinor: true,
85
- });
86
- this.constReactance.show();
87
- this.constConductance = new ConstConductance({
88
- data: gridData,
89
- scaler: this.scalers.default,
90
- showMinor: true,
91
- });
92
- this.constConductance.hide();
93
- this.constSusceptance = new ConstSusceptance({
94
- data: gridData,
95
- scaler: this.scalers.default,
96
- showMinor: true,
97
- });
98
- this.constSusceptance.hide();
99
- this.constQCircles = new ConstQCircles(this.scalers.default);
100
- this.constQCircles.hide();
101
- this.constSwrCircles = new ConstSwrCircles(this.scalers.default);
102
- this.constSwrCircles.hide();
103
- this.cursor = this.initCursor();
104
- const cursorContainer = this.cursorContainer();
105
- this.reactanceAxis = this.drawReactanceAxis({
106
- stroke: '#334155',
107
- strokeWidth: '1',
108
- fill: 'none',
109
- });
110
- this.dataContainer = new SmithGroup().attr('data-layer', 'samples');
111
- this.markerContainer = new SmithGroup().attr('data-layer', 'markers');
112
- // build chart
113
- this.svg.append(this.container);
114
- this.container.append(this.constConductance.draw().attr('data-layer', 'conductance'));
115
- this.container.append(this.constSusceptance.draw().attr('data-layer', 'susceptance'));
116
- this.container.append(this.constResistance.draw().attr('data-layer', 'resistance'));
117
- this.container.append(this.constReactance.draw().attr('data-layer', 'reactance'));
118
- this.container.append(this.constQCircles.draw());
119
- this.container.append(this.constSwrCircles.draw());
120
- this.container.append(this.peripheralScaleRenderer);
121
- this.container.append(this.cursor.Group);
122
- this.container.append(this.reactanceAxis);
123
- this.container.append(cursorContainer);
124
- this.container.append(this.dataContainer);
125
- this.container.append(this.markerContainer);
126
- const labels = new SmithGroup().attr('data-layer', 'labels').attr('pointer-events', 'none');
127
- for (const [name, layer] of [
128
- ['conductance', this.constConductance],
129
- ['susceptance', this.constSusceptance],
130
- ['resistance', this.constResistance],
131
- ['reactance', this.constReactance],
132
- ]) {
133
- 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
+ }
134
59
  }
135
- labels.append(this.peripheralScaleRenderer.labels);
136
- this.container.append(labels);
137
- const assertAlive = () => this.assertAlive();
138
- this.layers = {
139
- resistance: gridLayer(this.constResistance, assertAlive),
140
- reactance: gridLayer(this.constReactance, assertAlive),
141
- conductance: gridLayer(this.constConductance, assertAlive),
142
- susceptance: gridLayer(this.constSusceptance, assertAlive),
143
- q: circleLayer(this.constQCircles, 0, assertAlive),
144
- vswr: circleLayer(this.constSwrCircles, 1, assertAlive),
145
- };
146
- this.peripheralScales = {
147
- setVisible: (visible) => {
148
- assertAlive();
149
- if (visible) {
150
- this.peripheralScaleRenderer.show();
151
- }
152
- else {
153
- this.peripheralScaleRenderer.hide();
154
- }
155
- },
156
- update: (gamma) => {
157
- assertAlive();
158
- this.peripheralScaleRenderer.update(gamma);
159
- },
160
- };
161
- this.initializeZoom();
162
- this.resizeObserver = new ResizeObserver(() => this.updateViewportScale());
163
- this.resizeObserver.observe(this.svg.Node);
164
60
  }
165
61
  draw(target) {
166
62
  this.assertAlive();
167
- const host = typeof target === 'string' ? document.querySelector(target) : target;
168
- if (!host) {
169
- throw new Error('Chart container was not found.');
63
+ this.renderer.draw(target);
64
+ }
65
+ /** Export the current mounted view as standalone SVG with its configured background. */
66
+ toSvg(options) {
67
+ this.assertAlive();
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 }));
170
96
  }
171
- host.appendChild(this.svg.Node);
172
- this.updateViewportScale();
97
+ return ImageExporter.png(sources, options, this.exportLegend(options));
173
98
  }
174
99
  /** Remove this chart and release its event handlers. Safe to call more than once. */
175
100
  destroy() {
176
101
  if (this.destroyed) {
177
102
  return;
178
103
  }
179
- this.resizeObserver.disconnect();
180
104
  this.listeners.clear();
181
105
  this.clearTraces();
182
106
  this.destroyed = true;
183
- this.cursor.setMoveHandler(null);
184
- this.mouseGesture.destroy();
185
- this.zoomBehavior.on('start', null).on('zoom', null);
186
- this.svg.Element.interrupt().on('.zoom', null);
187
- this.svg.Element.selectAll('*').interrupt().on('.smithkit', null).on('.drag', null);
188
- this.svg.Element.remove();
189
- }
190
- updateViewportScale() {
191
- if (this.destroyed) {
192
- return;
193
- }
194
- const matrix = this.svg.Node.getScreenCTM();
195
- if (matrix) {
196
- const scale = Math.hypot(matrix.a, matrix.b);
197
- this.data.forEach((trace) => trace.setViewportScale(scale));
198
- }
107
+ this.renderer.destroy();
199
108
  }
200
109
  assertAlive() {
201
110
  if (this.destroyed) {
202
111
  throw new Error('This Smith chart has been destroyed. Create a new instance.');
203
112
  }
204
113
  }
205
- createScalers(size) {
206
- const impedance = new SmithScaler(d3.scaleLinear().domain([-1, 1]).range([0, size]), d3.scaleLinear().domain([1, -1]).range([0, size]), d3
207
- .scaleLinear()
208
- .domain([0, 1])
209
- .range([0, size / 2]));
210
- const admittance = new SmithScaler(d3.scaleLinear().domain([1, -1]).range([0, size]), d3.scaleLinear().domain([-1, 1]).range([0, size]), d3
211
- .scaleLinear()
212
- .domain([0, 1])
213
- .range([0, size / 2]));
214
- return { default: impedance, impedance, admittance };
215
- }
216
- cursorMove(p) {
217
- if (this.draggedMarkers.size > 0) {
218
- return;
219
- }
220
- this.cursor.Position = Complex.from(this.scalers.default.pointInvert(p));
221
- }
222
114
  markerDragChanged(marker, dragging) {
223
115
  const wasDragging = this.draggedMarkers.size > 0;
224
116
  if (dragging) {
@@ -231,104 +123,23 @@ export class Smith {
231
123
  if (isDragging === wasDragging) {
232
124
  return;
233
125
  }
126
+ this.renderer.setMarkerDragging(isDragging);
234
127
  if (isDragging) {
235
- this.cursorBeforeMarkerDrag = this.svg.Node.style.getPropertyValue('cursor') || null;
236
- this.svg.Element.style('cursor', 'grabbing').style('--smithkit-marker-cursor', 'grabbing');
237
- this.cursor.hide();
238
128
  this.emit({ type: SmithEventType.Cursor, data: undefined });
239
129
  }
240
- else {
241
- this.svg.Element.style('cursor', () => this.cursorBeforeMarkerDrag).style('--smithkit-marker-cursor', null);
242
- this.cursorBeforeMarkerDrag = null;
243
- }
244
- }
245
- initCursor() {
246
- const cursor = new SmithCursor(this.scalers.default);
247
- cursor.Group.attr('class', 'smith-cursor');
248
- cursor.setMoveHandler(() => {
249
- this.emit({ type: SmithEventType.Cursor, data: this.cursorReading });
250
- });
251
- return cursor;
252
130
  }
253
131
  /** Last cursor position. Use cursor events to detect pointer leave. */
254
132
  get cursorReading() {
255
- return readReflection(this.cursor.Position, this.referenceImpedanceOhms);
256
- }
257
- initializeZoom() {
258
- const zoom = this.zoomBehavior
259
- .scaleExtent([0.6, 1000])
260
- .filter((event) => this.zoomEnabled && (!event.ctrlKey || event.type === 'wheel') && !event.button)
261
- .on('start', (event) => {
262
- if (event.sourceEvent) {
263
- this.mouseGesture.capture(event.sourceEvent, 'zoom');
264
- }
265
- })
266
- .on('zoom', (event) => {
267
- if (!this.zoomEnabled && !this.applyingView) {
268
- // An already active mouse/touch gesture still needs its normal end event.
269
- // Restore D3's view through its public API while that gesture finishes.
270
- this.applyView(this.transform);
271
- return;
272
- }
273
- this.onZoom(event.transform);
274
- });
275
- this.svg.Element.call(zoom);
276
- this.resetView();
133
+ return RfCalculations.readReflection(this.renderer.cursorPosition, this.referenceOhms);
277
134
  }
278
135
  /** Enable or disable wheel, double-click, and mouse/touch zoom/pan. Keeps the current view. */
279
136
  setZoomEnabled(enabled) {
280
137
  this.assertAlive();
281
- if (typeof enabled !== 'boolean') {
282
- throw new TypeError('Zoom enabled must be a boolean.');
283
- }
284
- if (this.zoomEnabled === enabled) {
285
- return;
286
- }
287
- this.zoomEnabled = enabled;
288
- if (!enabled) {
289
- this.svg.Element.interrupt();
290
- }
291
- }
292
- applyView(transform) {
293
- this.applyingView = true;
294
- try {
295
- this.svg.Element.call(this.zoomBehavior.transform, transform);
296
- }
297
- finally {
298
- this.applyingView = false;
299
- }
138
+ this.renderer.setZoomEnabled(enabled);
300
139
  }
301
140
  resetView() {
302
141
  this.assertAlive();
303
- const transform = d3.zoomIdentity.translate(62.5, 62.5).scale(0.75);
304
- this.applyView(transform);
305
- }
306
- onZoom(transform) {
307
- if (this.destroyed) {
308
- return;
309
- }
310
- this.transform = transform;
311
- this.container.Element.attr('transform', transform.toString());
312
- this.data.forEach((d) => d.zoom(transform));
313
- }
314
- cursorContainer() {
315
- const shape = this.drawReactanceAxis({ fill: 'transparent', stroke: 'none' });
316
- shape.Element.style('pointer-events', 'all')
317
- .on('pointermove.smithkit', (event) => {
318
- this.cursorMove(d3.pointer(event));
319
- })
320
- .on('pointerleave.smithkit', () => {
321
- this.cursor.hide();
322
- this.emit({ type: SmithEventType.Cursor, data: undefined });
323
- });
324
- return shape;
325
- }
326
- drawReactanceAxis(opts) {
327
- const c = this.calcs.resistanceCircle(0);
328
- c.p[0] = this.scalers.default.x(c.p[0]);
329
- c.p[1] = this.scalers.default.y(c.p[1]);
330
- c.r = this.scalers.default.r(c.r);
331
- return new SmithCircle(c, opts);
142
+ this.renderer.resetView();
332
143
  }
333
144
  /** Add a named trace with one initial marker. Returns a chart-local, stable ID. */
334
145
  addTrace(values, options = {}) {
@@ -340,7 +151,6 @@ export class Smith {
340
151
  const data = this.createSmithData(values, this.nextDatasetColor, options);
341
152
  this.nextDatasetColor++;
342
153
  this.data.push(data);
343
- this.updateViewportScale();
344
154
  const id = this.traceMetadata.get(data).id;
345
155
  this.setTraceOptions(id, options);
346
156
  return id;
@@ -348,7 +158,8 @@ export class Smith {
348
158
  /** Detached metadata snapshots; IDs and marker display numbers survive removals. */
349
159
  getTraces() {
350
160
  return this.data.map((data) => ({
351
- ...this.traceMetadata.get(data),
161
+ id: this.traceMetadata.get(data).id,
162
+ name: this.traceMetadata.get(data).name,
352
163
  ...data.Style,
353
164
  color: data.Color,
354
165
  visible: data.Visible,
@@ -394,7 +205,7 @@ export class Smith {
394
205
  throw new TypeError('Trace name must not be empty.');
395
206
  }
396
207
  if (options.color !== undefined &&
397
- (typeof options.color !== 'string' || !d3.color(options.color))) {
208
+ (typeof options.color !== 'string' || !parseColor(options.color))) {
398
209
  throw new TypeError('Trace color must be a solid CSS color.');
399
210
  }
400
211
  if (options.visible !== undefined && typeof options.visible !== 'boolean') {
@@ -411,9 +222,11 @@ export class Smith {
411
222
  data.setStyle(options);
412
223
  if (options.name !== undefined) {
413
224
  this.traceMetadata.get(data).name = options.name.trim();
225
+ data.setName(options.name.trim());
414
226
  }
415
227
  if (options.color !== undefined) {
416
228
  data.setColor(options.color);
229
+ delete this.traceMetadata.get(data).colorIndex;
417
230
  }
418
231
  if (options.visible !== undefined) {
419
232
  data.setVisible(options.visible);
@@ -440,7 +253,7 @@ export class Smith {
440
253
  if (index < 0) {
441
254
  return false;
442
255
  }
443
- this.data[index].destroy();
256
+ this.renderer.removeTrace(this.data[index]);
444
257
  this.data.splice(index, 1);
445
258
  return true;
446
259
  }
@@ -459,6 +272,14 @@ export class Smith {
459
272
  const location = this.findMarker(id);
460
273
  return location ? this.data[location.datasetNo].removeMarker(location.markerNo) : false;
461
274
  }
275
+ /** Focus a mounted, visible marker for keyboard interaction. */
276
+ focusMarker(id) {
277
+ this.assertAlive();
278
+ const location = this.findMarker(id);
279
+ return location
280
+ ? this.data[location.datasetNo].Markers[location.markerNo].marker.focus()
281
+ : false;
282
+ }
462
283
  setMarkerSample(id, sampleIndex) {
463
284
  this.assertAlive();
464
285
  const location = this.findMarker(id);
@@ -482,7 +303,7 @@ export class Smith {
482
303
  const data = this.data[location.datasetNo];
483
304
  const marker = data.Markers[location.markerNo];
484
305
  return {
485
- ...readReflection(Complex.from(...marker.selectedPoint.reflectionCoefficient), this.referenceImpedanceOhms),
306
+ ...RfCalculations.readReflection(Complex.from(...marker.selectedPoint.reflectionCoefficient), this.referenceImpedanceOhms),
486
307
  frequencyHz: marker.selectedPoint.frequencyHz,
487
308
  traceId: this.traceMetadata.get(data).id,
488
309
  markerId: id,
@@ -494,7 +315,7 @@ export class Smith {
494
315
  compareMarkers(a, b) {
495
316
  const first = this.getMarker(a);
496
317
  const second = this.getMarker(b);
497
- return first && second ? compareMarkerReadings(first, second) : undefined;
318
+ return first && second ? MarkerMeasurements.compare(first, second) : undefined;
498
319
  }
499
320
  /** Current positive real reference impedance, in ohms. */
500
321
  get referenceImpedanceOhms() {
@@ -504,25 +325,25 @@ export class Smith {
504
325
  renormalize(referenceImpedanceOhms) {
505
326
  this.assertAlive();
506
327
  // Validate even with no traces, and prepare every result before changing any state.
507
- renormalizeSamples([], this.referenceOhms, referenceImpedanceOhms);
328
+ RfCalculations.renormalizeSamples([], this.referenceOhms, referenceImpedanceOhms);
508
329
  if (referenceImpedanceOhms === this.referenceOhms) {
509
330
  return;
510
331
  }
511
- const samples = this.data.map((data) => renormalizeSamples(data.Samples, this.referenceOhms, referenceImpedanceOhms));
332
+ const samples = this.data.map((data) => RfCalculations.renormalizeSamples(data.Samples, this.referenceOhms, referenceImpedanceOhms));
512
333
  this.data.forEach((data) => data.Markers.forEach((entry) => entry.marker.cancelDrag()));
513
334
  this.referenceOhms = referenceImpedanceOhms;
514
335
  this.data.forEach((data, index) => data.update(samples[index], 'sample-index'));
515
- this.cursor.hide();
336
+ this.renderer.hideCursor();
516
337
  this.emit({ type: SmithEventType.Cursor, data: undefined });
517
338
  }
518
339
  clearTraces() {
519
340
  this.assertAlive();
520
- this.data.forEach((dataset) => dataset.destroy());
341
+ this.data.forEach((dataset) => this.renderer.removeTrace(dataset));
521
342
  this.data = [];
522
343
  }
523
344
  createSmithData(values, dataset, options) {
524
- const color = d3.schemeCategory10[(1 + dataset) % d3.schemeCategory10.length];
525
- const data = new SmithData(values, color, this.transform, this.dataContainer, this.scalers.default, (marker, dragging) => {
345
+ const color = this.theme.traceColors[dataset % this.theme.traceColors.length];
346
+ const data = this.renderer.createTrace(values, color, options, (marker, dragging) => {
526
347
  this.markerDragChanged(marker, dragging);
527
348
  const snapshot = this.getMarker(this.markerId(marker));
528
349
  if (snapshot) {
@@ -531,9 +352,20 @@ export class Smith {
531
352
  data: snapshot,
532
353
  });
533
354
  }
534
- }, this.markerContainer, options);
355
+ });
535
356
  const number = this.nextTraceId++;
536
- this.traceMetadata.set(data, { id: `trace-${number}`, name: `Trace ${number}` });
357
+ this.traceMetadata.set(data, {
358
+ id: `trace-${number}`,
359
+ name: `Trace ${number}`,
360
+ colorIndex: dataset,
361
+ });
362
+ data.setName(`Trace ${number}`);
363
+ data.setMarkerSelectHandler((marker) => {
364
+ const snapshot = this.getMarker(this.markerId(marker));
365
+ if (snapshot) {
366
+ this.emit({ type: SmithEventType.MarkerSelect, data: snapshot });
367
+ }
368
+ });
537
369
  data.setMarkerMoveHandler((index) => {
538
370
  const marker = data.Markers[index];
539
371
  const snapshot = marker && this.getMarker(this.markerId(marker.marker));
@@ -0,0 +1,10 @@
1
+ import type { Complex } from './math/Complex.js';
2
+ export declare class SmithFormatter {
3
+ private constructor();
4
+ /** Format with three significant digits and an SI prefix; includes spacing before the unit. */
5
+ static number(value: number): string;
6
+ /** Format Cartesian components to decimalPlaces fractional digits. */
7
+ static complex(value: Complex, unit?: string, decimalPlaces?: number): string;
8
+ /** Format magnitude and phase in degrees to decimalPlaces fractional digits. */
9
+ static polar(value: Complex, unit?: string, decimalPlaces?: number): string;
10
+ }
@@ -0,0 +1,19 @@
1
+ import { format } from 'd3';
2
+ export class SmithFormatter {
3
+ constructor() { }
4
+ /** Format with three significant digits and an SI prefix; includes spacing before the unit. */
5
+ static number(value) {
6
+ const formatted = format('.3~s')(value);
7
+ return Number.isFinite(value) && /[a-zA-Zµ]$/.test(formatted)
8
+ ? formatted.replace(/([a-zA-Zµ])$/, ' $1')
9
+ : formatted + ' ';
10
+ }
11
+ /** Format Cartesian components to decimalPlaces fractional digits. */
12
+ static complex(value, unit = '', decimalPlaces = 3) {
13
+ return `${value.toString(decimalPlaces)} ${unit === '' ? '' : `[${unit}]`}`;
14
+ }
15
+ /** Format magnitude and phase in degrees to decimalPlaces fractional digits. */
16
+ static polar(value, unit = '', decimalPlaces = 3) {
17
+ return `${value.abs().toFixed(decimalPlaces)} ${unit} ∠${((value.arg() * 180) / Math.PI).toFixed(decimalPlaces)}°`;
18
+ }
19
+ }
@@ -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
+ }