smithkit 0.1.1 → 0.2.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 (132) hide show
  1. package/README.md +119 -40
  2. package/dist/lib/MarkerMeasurements.d.ts +6 -0
  3. package/dist/lib/MarkerMeasurements.js +22 -0
  4. package/dist/lib/Smith.d.ts +9 -2
  5. package/dist/lib/Smith.js +68 -36
  6. package/dist/lib/SmithFormatter.d.ts +10 -0
  7. package/dist/lib/SmithFormatter.js +19 -0
  8. package/dist/lib/grid/CircleLayerControl.d.ts +15 -0
  9. package/dist/lib/grid/CircleLayerControl.js +40 -0
  10. package/dist/lib/{draw → grid}/ConstCircles.d.ts +4 -6
  11. package/dist/lib/{draw → grid}/ConstCircles.js +2 -2
  12. package/dist/lib/{draw → grid}/ConstQCircles.d.ts +2 -2
  13. package/dist/lib/{draw → grid}/ConstQCircles.js +3 -3
  14. package/dist/lib/{draw → grid}/ConstSwrCircles.d.ts +2 -2
  15. package/dist/lib/{draw → grid}/ConstSwrCircles.js +3 -3
  16. package/dist/lib/grid/GridDefinitions.d.ts +12 -0
  17. package/dist/lib/grid/GridDefinitions.js +138 -0
  18. package/dist/lib/grid/GridGeometry.d.ts +26 -0
  19. package/dist/lib/grid/GridGeometry.js +58 -0
  20. package/dist/lib/grid/GridLabels.d.ts +8 -0
  21. package/dist/lib/grid/GridLabels.js +45 -0
  22. package/dist/lib/grid/GridLayerControl.d.ts +12 -0
  23. package/dist/lib/grid/GridLayerControl.js +39 -0
  24. package/dist/lib/grid/LayerControl.d.ts +6 -0
  25. package/dist/lib/grid/LayerControl.js +16 -0
  26. package/dist/lib/{draw → grid}/SmithGridLayer.d.ts +5 -4
  27. package/dist/lib/{draw → grid}/SmithGridLayer.js +26 -9
  28. package/dist/lib/grid/types.d.ts +1 -1
  29. package/dist/lib/index.d.ts +8 -10
  30. package/dist/lib/index.js +5 -7
  31. package/dist/lib/{draw → interaction}/SmithCursor.d.ts +3 -3
  32. package/dist/lib/{draw → interaction}/SmithCursor.js +6 -6
  33. package/dist/lib/io/Touchstone.d.ts +10 -0
  34. package/dist/lib/io/Touchstone.js +75 -0
  35. package/dist/lib/layers.d.ts +2 -8
  36. package/dist/lib/layers.js +1 -73
  37. package/dist/lib/{complex → math}/Complex.d.ts +1 -0
  38. package/dist/lib/{complex → math}/Complex.js +1 -1
  39. package/dist/lib/math/geometry.d.ts +14 -0
  40. package/dist/lib/measurements.d.ts +2 -4
  41. package/dist/lib/measurements.js +1 -19
  42. package/dist/lib/rf/RfCalculations.d.ts +65 -0
  43. package/dist/lib/rf/RfCalculations.js +161 -0
  44. package/dist/lib/{SmithConstantCircle.d.ts → rf/SmithConstantCircle.d.ts} +2 -3
  45. package/dist/lib/{SmithConstantCircle.js → rf/SmithConstantCircle.js} +3 -4
  46. package/dist/lib/scales/PeripheralScaleDefinitions.d.ts +16 -0
  47. package/dist/lib/scales/PeripheralScaleDefinitions.js +54 -0
  48. package/dist/lib/scales/RadialScaleDefinitions.d.ts +15 -0
  49. package/dist/lib/scales/RadialScaleDefinitions.js +112 -0
  50. package/dist/lib/scales/SmithPeripheralScales.d.ts +4 -2
  51. package/dist/lib/scales/SmithPeripheralScales.js +46 -26
  52. package/dist/lib/scales/SmithScales.d.ts +3 -1
  53. package/dist/lib/scales/SmithScales.js +8 -2
  54. package/dist/lib/svg/LabelLayout.d.ts +17 -0
  55. package/dist/lib/svg/LabelLayout.js +145 -0
  56. package/dist/lib/{draw → svg}/SmithArc.d.ts +1 -1
  57. package/dist/lib/{draw → svg}/SmithCircle.d.ts +1 -1
  58. package/dist/lib/{draw → svg}/SmithLine.d.ts +1 -1
  59. package/dist/lib/{draw → svg}/SmithScaler.d.ts +1 -4
  60. package/dist/lib/{draw → svg}/SmithText.d.ts +1 -1
  61. package/dist/lib/svg/SvgExporter.d.ts +14 -0
  62. package/dist/lib/svg/SvgExporter.js +137 -0
  63. package/dist/lib/{draw → traces}/SmithData.d.ts +24 -35
  64. package/dist/lib/traces/SmithData.js +195 -0
  65. package/dist/lib/{draw → traces}/SmithMarker.d.ts +14 -3
  66. package/dist/lib/traces/SmithMarker.js +237 -0
  67. package/dist/lib/traces/TraceModel.d.ts +31 -0
  68. package/dist/lib/traces/TraceModel.js +123 -0
  69. package/dist/lib/traces/TraceRenderer.d.ts +33 -0
  70. package/dist/lib/traces/TraceRenderer.js +152 -0
  71. package/package.json +4 -1
  72. package/dist/lib/SmithArcsDefs.d.ts +0 -10
  73. package/dist/lib/SmithArcsDefs.js +0 -10
  74. package/dist/lib/components.d.ts +0 -14
  75. package/dist/lib/components.js +0 -26
  76. package/dist/lib/draw/SmithData.js +0 -366
  77. package/dist/lib/draw/SmithMarker.js +0 -122
  78. package/dist/lib/formatting.d.ts +0 -7
  79. package/dist/lib/formatting.js +0 -16
  80. package/dist/lib/grid/definitions.d.ts +0 -3
  81. package/dist/lib/grid/definitions.js +0 -123
  82. package/dist/lib/grid/geometry.d.ts +0 -20
  83. package/dist/lib/grid/geometry.js +0 -55
  84. package/dist/lib/grid/labels.d.ts +0 -3
  85. package/dist/lib/grid/labels.js +0 -41
  86. package/dist/lib/io/touchstone.d.ts +0 -7
  87. package/dist/lib/io/touchstone.js +0 -72
  88. package/dist/lib/math/complexRatio.d.ts +0 -3
  89. package/dist/lib/math/complexRatio.js +0 -30
  90. package/dist/lib/renormalization.d.ts +0 -10
  91. package/dist/lib/renormalization.js +0 -62
  92. package/dist/lib/rf.d.ts +0 -35
  93. package/dist/lib/rf.js +0 -74
  94. package/dist/lib/scales/peripheralScales.d.ts +0 -12
  95. package/dist/lib/scales/peripheralScales.js +0 -49
  96. package/dist/lib/scales/radialScales.d.ts +0 -12
  97. package/dist/lib/scales/radialScales.js +0 -109
  98. package/dist/lib/shapes/Arc.d.ts +0 -6
  99. package/dist/lib/shapes/Circle.d.ts +0 -5
  100. package/dist/lib/shapes/Circle.js +0 -1
  101. package/dist/lib/shapes/Line.d.ts +0 -5
  102. package/dist/lib/shapes/Line.js +0 -1
  103. package/dist/lib/shapes/Point.d.ts +0 -1
  104. package/dist/lib/shapes/Point.js +0 -1
  105. /package/dist/lib/{draw → grid}/ConstConductance.d.ts +0 -0
  106. /package/dist/lib/{draw → grid}/ConstConductance.js +0 -0
  107. /package/dist/lib/{draw → grid}/ConstReactance.d.ts +0 -0
  108. /package/dist/lib/{draw → grid}/ConstReactance.js +0 -0
  109. /package/dist/lib/{draw → grid}/ConstResistance.d.ts +0 -0
  110. /package/dist/lib/{draw → grid}/ConstResistance.js +0 -0
  111. /package/dist/lib/{draw → grid}/ConstSusceptance.d.ts +0 -0
  112. /package/dist/lib/{draw → grid}/ConstSusceptance.js +0 -0
  113. /package/dist/lib/{arcs → grid}/Tick.d.ts +0 -0
  114. /package/dist/lib/{arcs → grid}/Tick.js +0 -0
  115. /package/dist/lib/{draw → interaction}/MouseGesture.d.ts +0 -0
  116. /package/dist/lib/{draw → interaction}/MouseGesture.js +0 -0
  117. /package/dist/lib/{draw/SmithDrawOptions.js → math/geometry.js} +0 -0
  118. /package/dist/lib/{draw → svg}/SmithArc.js +0 -0
  119. /package/dist/lib/{draw → svg}/SmithCircle.js +0 -0
  120. /package/dist/lib/{draw → svg}/SmithDrawOptions.d.ts +0 -0
  121. /package/dist/lib/{shapes/Arc.js → svg/SmithDrawOptions.js} +0 -0
  122. /package/dist/lib/{draw → svg}/SmithGroup.d.ts +0 -0
  123. /package/dist/lib/{draw → svg}/SmithGroup.js +0 -0
  124. /package/dist/lib/{draw → svg}/SmithLine.js +0 -0
  125. /package/dist/lib/{draw → svg}/SmithPath.d.ts +0 -0
  126. /package/dist/lib/{draw → svg}/SmithPath.js +0 -0
  127. /package/dist/lib/{draw → svg}/SmithScaler.js +0 -0
  128. /package/dist/lib/{draw → svg}/SmithShape.d.ts +0 -0
  129. /package/dist/lib/{draw → svg}/SmithShape.js +0 -0
  130. /package/dist/lib/{draw → svg}/SmithSvg.d.ts +0 -0
  131. /package/dist/lib/{draw → svg}/SmithSvg.js +0 -0
  132. /package/dist/lib/{draw → svg}/SmithText.js +0 -0
package/README.md CHANGED
@@ -5,17 +5,22 @@ interactive SVG Smith charts, RF calculations, and S11 measurement exploration.
5
5
 
6
6
  [Try the interactive demo](https://rosek86.github.io/smithkit/).
7
7
 
8
+ Upgrading from 0.1.x? Version 0.2.0 groups calculation, parsing, formatting, and
9
+ comparison functions into classes. See the [migration guide](https://github.com/rosek86/smithkit/blob/v0.2.0/docs/migration-0.2.md)
10
+ for the changed imports.
11
+
8
12
  ## Features
9
13
 
10
14
  - Labeled resistance, reactance, conductance, and susceptance grids.
11
15
  - Configurable constant-Q and constant-VSWR circles.
12
16
  - Zoom, pan, view reset, and cursor readings.
13
- - Named traces with colors, visibility, and draggable markers that snap to samples.
17
+ - Named traces with colors, visibility, and keyboard/touch/mouse markers that snap to samples.
14
18
  - Marker comparisons: frequency, complex impedance, and wrapped phase differences.
15
19
  - Physical impedance/admittance conversions and DOM-independent RF readings.
16
20
  - Twelve parameter scales mounted independently of chart zoom.
17
21
  - Four peripheral rulers for phase and electrical length.
18
22
  - One-port Touchstone 1.x import with RI, MA, and DB representations.
23
+ - Standalone SVG export of the current chart and radial scales.
19
24
  - ESM modules and TypeScript declarations.
20
25
 
21
26
  ## Installation
@@ -63,11 +68,11 @@ must be finite and non-negative; both coordinates must be finite. A trace must
63
68
  contain at least one sample.
64
69
 
65
70
  Each new trace receives one marker on its first sample. Drag markers to select
66
- samples, scroll to zoom, and drag the chart to pan. Markers stay above all sample
67
- points. `chart.resetView()` restores the initial view, including peripheral rulers.
71
+ samples with a mouse or touch, scroll to zoom, and drag the chart to pan. Markers
72
+ and their focus indicators stay above grid labels and sample points. `chart.resetView()` restores the initial view, including peripheral rulers.
68
73
 
69
74
  Construct `Smith` only in a browser, such as your framework's mount hook. Importing
70
- the package and using its calculation or parsing functions does not require a DOM.
75
+ the package and using its calculation or parsing methods does not require a DOM.
71
76
 
72
77
  Zoom is enabled by default. For a fixed view, call `chart.setZoomEnabled(false)` before or after mounting.
73
78
  It disables wheel/double-click zoom and mouse/touch panning and pinch zoom while
@@ -89,6 +94,25 @@ chart.draw('#smith');
89
94
  container throws. Call `chart.destroy()` on unmount; it removes the SVG, releases
90
95
  event handlers, and cancels queued notifications. Repeated destruction is safe.
91
96
 
97
+ ### Keyboard and touch markers
98
+
99
+ Use Tab/Shift+Tab to focus a marker. Arrow keys move through samples in input order;
100
+ Right/Up advances and Left/Down goes back. Shift+Arrow or Page Up/Down moves ten
101
+ samples, and Home/End selects the first/last sample. Navigation clamps at the ends
102
+ and also works when chart zoom is disabled. A single-sample trace remains focusable
103
+ for reading, with its slider marked disabled.
104
+
105
+ A focused marker has a contrasting outline and exposes its trace name, marker
106
+ number, sample position, and frequency to assistive technology. Hidden traces are
107
+ excluded from keyboard focus. `chart.focusMarker(id)` allows a separate marker
108
+ selector to focus the matching chart control. The demo provides a **Focus on chart**
109
+ button beside its marker selector.
110
+
111
+ Markers have a 44 CSS px circular hit area and an 18 CSS px triangle, kept constant
112
+ through zoom and container resizing. A marker touch gesture does not pan the chart;
113
+ cancelling it releases the drag state. Touch target areas may overlap for nearby
114
+ markers; use keyboard focus or the demo selector to reach an obscured marker.
115
+
92
116
  ## Traces and markers
93
117
 
94
118
  Trace and marker IDs are opaque strings, unique within a chart and never reused
@@ -121,6 +145,7 @@ chart.removeMarker(markerB);
121
145
  | `clearTraces()` | Removes all traces and their markers. |
122
146
  | `addMarker(traceId, sampleIndex = 0)` | Returns a marker ID, or `undefined` if the trace is missing. |
123
147
  | `getMarker(id)` | Returns a `MarkerSnapshot`, or `undefined` if missing. |
148
+ | `focusMarker(id)` | Moves keyboard focus to a mounted, visible marker; returns `false` if missing or not focusable. |
124
149
  | `setMarkerSample(id, sampleIndex)` | Selects an existing sample and queues a marker notification. |
125
150
  | `removeMarker(id)` | Removes one marker without renumbering the remaining markers. |
126
151
  | `compareMarkers(a, b)` | Returns B − A, or `undefined` if either marker is missing. |
@@ -192,7 +217,7 @@ const unsubscribe = chart.onEvent((event) => {
192
217
  console.log(event.data?.impedanceOhms?.toVector());
193
218
  return;
194
219
  }
195
- // Marker, MarkerDragStart, and MarkerDragEnd always carry a MarkerSnapshot.
220
+ // Marker, MarkerSelect, MarkerDragStart, and MarkerDragEnd carry a MarkerSnapshot.
196
221
  console.log(event.data.markerId, event.data.frequencyHz);
197
222
  });
198
223
 
@@ -202,7 +227,12 @@ chart.destroy();
202
227
  ```
203
228
 
204
229
  The event types are `Cursor` (`'cursor'`), `Marker` (`'marker'`),
230
+ `MarkerSelect` (`'marker-select'`),
205
231
  `MarkerDragStart` (`'marker-drag-start'`), and `MarkerDragEnd` (`'marker-drag-end'`).
232
+ `MarkerSelect` fires synchronously when a marker receives focus or is engaged by
233
+ keyboard navigation or a pointer press. Use it to select the corresponding
234
+ readout; it does not indicate a position change. Keyboard changes use the normal
235
+ queued `Marker` event and do not emit drag lifecycle events.
206
236
  Cursor movement is suspended during marker dragging. Drag start/end events fire
207
237
  synchronously; marker position notifications are queued and report the latest
208
238
  snapshot at delivery. Before delivery, repeated updates to the same marker coalesce
@@ -213,7 +243,7 @@ Trace metadata changes do not emit marker events; refresh metadata after mutatio
213
243
  Destruction suppresses application callbacks.
214
244
 
215
245
  `chart.cursorReading` returns the last cursor position. Cursor and marker readings
216
- share `SmithReading`; `readReflection(gamma, referenceImpedanceOhms = 50)` produces
246
+ share `SmithReading`; `RfCalculations.readReflection(gamma, referenceImpedanceOhms = 50)` produces
217
247
  the same reading without constructing a chart.
218
248
 
219
249
  | Fields | Units / meaning |
@@ -239,7 +269,7 @@ infinite scalar limit is preserved: perfect match gives infinite return loss;
239
269
  For active loads (\|Γ\| > 1), VSWR, standing-wave quantities, reflection loss,
240
270
  attenuation, and power transmission are `undefined`. Other quantities remain
241
271
  available. Non-finite complex inputs or invalid reference impedances throw
242
- `RangeError` in the public RF functions. Unrepresentable complex conversion
272
+ `RangeError` in the public RF methods. Unrepresentable complex conversion
243
273
  results are `undefined`.
244
274
 
245
275
  ## Selecting a marker by frequency
@@ -259,22 +289,22 @@ measurement frequency; dragging and sample selection keep that field synchronize
259
289
  ## Calculations without a chart
260
290
 
261
291
  ```ts
262
- import { Complex, readReflection, impedanceToReflection } from 'smithkit';
292
+ import { Complex, RfCalculations } from 'smithkit';
263
293
 
264
- const gamma = impedanceToReflection(Complex.from(75, 25), 50)!;
265
- const reading = readReflection(gamma, 50);
294
+ const gamma = RfCalculations.impedanceToReflection(Complex.from(75, 25), 50)!;
295
+ const reading = RfCalculations.readReflection(gamma, 50);
266
296
  console.log(reading.impedanceOhms?.toVector()); // Approximately [75, 25].
267
297
  console.log(reading.admittanceSiemens?.toVector());
268
298
  ```
269
299
 
270
- | Function | Input and output |
271
- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
272
- | `reflectionToImpedance(gamma, referenceImpedanceOhms = 50)` | Γ → physical Z in Ω. |
273
- | `impedanceToReflection(impedanceOhms, referenceImpedanceOhms = 50)` | Physical Z in Ω → Γ. |
274
- | `reflectionToAdmittance(gamma, referenceImpedanceOhms = 50)` | Γ → physical Y in S. |
275
- | `admittanceToReflection(admittanceSiemens, referenceImpedanceOhms = 50)` | Physical Y in S → Γ. |
276
- | `readReflection(gamma, referenceImpedanceOhms = 50)` | Complete `SmithReading`. |
277
- | `compareMarkerReadings(a, b)` | Compare readings without a chart. Each input needs `frequencyHz`, `reflectionCoefficient`, and `impedanceOhms`. |
300
+ | Static method | Input and output |
301
+ | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
302
+ | `RfCalculations.reflectionToImpedance(gamma, referenceImpedanceOhms = 50)` | Γ → physical Z in Ω. |
303
+ | `RfCalculations.impedanceToReflection(impedanceOhms, referenceImpedanceOhms = 50)` | Physical Z in Ω → Γ. |
304
+ | `RfCalculations.reflectionToAdmittance(gamma, referenceImpedanceOhms = 50)` | Γ → physical Y in S. |
305
+ | `RfCalculations.admittanceToReflection(admittanceSiemens, referenceImpedanceOhms = 50)` | Physical Y in S → Γ. |
306
+ | `RfCalculations.readReflection(gamma, referenceImpedanceOhms = 50)` | Complete `SmithReading`. |
307
+ | `MarkerMeasurements.compare(a, b)` | Compare readings without a chart. Each input needs `frequencyHz`, `reflectionCoefficient`, and `impedanceOhms`. |
278
308
 
279
309
  Comparison results contain `frequencyDeltaHz`, `impedanceDeltaOhms`, and
280
310
  `phaseDeltaDegrees`. Phase differences use B − A wrapped to **[−180°, 180°)**:
@@ -285,27 +315,27 @@ a point comparison, not sweep phase unwrapping or group delay.
285
315
 
286
316
  `Complex.from(re, im)` constructs complex numbers. Read components through `re` and `im`. Arithmetic (`add`, `sub`,
287
317
  `mul`, `div`, `pow`) returns new values. The class also provides `abs`, `arg`
288
- (radians), and static functions such as `Complex.conj`, `Complex.sqrt`,
318
+ (radians), and static methods such as `Complex.conj`, `Complex.sqrt`,
289
319
  `Complex.exp`, `Complex.log`, and trigonometric/hyperbolic functions and inverses. `toVector()` returns `[real, imaginary]`; `toString(dp)` formats
290
320
  a Cartesian value. See the exported declarations for all operations.
291
321
 
292
- Formatting helpers are independent exports: `formatNumber(value)` (three significant
293
- digits and SI prefixes), `formatComplex(value, unit?, decimalPlaces?)`, and
294
- `formatComplexPolar(value, unit?, decimalPlaces?)` (phase in degrees). They work
322
+ The `SmithFormatter` class provides static methods: `SmithFormatter.number(value)` (three significant
323
+ digits and SI prefixes), `SmithFormatter.complex(value, unit?, decimalPlaces?)`, and
324
+ `SmithFormatter.polar(value, unit?, decimalPlaces?)` (phase in degrees). They work
295
325
  without a chart or DOM. Complex formatters default to three fractional digits.
296
- `formatNumber` includes spacing so you can append a unit, e.g. `formatNumber(1e9) + 'Hz'`.
326
+ `SmithFormatter.number` includes spacing so you can append a unit, e.g. `SmithFormatter.number(1e9) + 'Hz'`.
297
327
 
298
- `reactanceToComponent(reactanceOhms, frequencyHz)` calculates an ideal equivalent
328
+ `RfCalculations.reactanceToComponent(reactanceOhms, frequencyHz)` calculates an ideal equivalent
299
329
  series component without formatting or constructing a chart:
300
330
 
301
331
  ```ts
302
- import { reactanceToComponent, formatNumber } from 'smithkit';
332
+ import { RfCalculations, SmithFormatter } from 'smithkit';
303
333
 
304
- const component = reactanceToComponent(-50, 1e9);
334
+ const component = RfCalculations.reactanceToComponent(-50, 1e9);
305
335
  if (component?.kind === 'capacitor') {
306
- console.log(formatNumber(component.capacitanceFarads) + 'F'); // 3.18 pF
336
+ console.log(SmithFormatter.number(component.capacitanceFarads) + 'F'); // 3.18 pF
307
337
  } else if (component?.kind === 'inductor') {
308
- console.log(formatNumber(component.inductanceHenries) + 'H');
338
+ console.log(SmithFormatter.number(component.inductanceHenries) + 'H');
309
339
  }
310
340
  ```
311
341
 
@@ -333,9 +363,10 @@ chart.layers.vswr.setVisible(true);
333
363
  The four grid layers support `setVisible`, `setMinorVisible`, and
334
364
  `setStyle(Partial<GridStyle>)`. Style fields: `stroke`, `majorWidth`, `minorWidth`,
335
365
  `textColor`, `textFontFamily`, `textFontSize`. Widths are positive finite numbers
336
- in CSS pixels. Font size is a positive finite number in chart units because labels
337
- scale with the grid geometry. Partial updates retain other settings. Invalid numeric
338
- styles throw before any property is changed.
366
+ in CSS pixels. Font size is a positive finite base size in chart units; adaptive
367
+ layout raises it when needed to keep the font size at least 9 CSS pixels in the
368
+ default view. Partial updates retain other settings. Invalid numeric styles throw
369
+ before any property is changed.
339
370
 
340
371
  `layers.q` and `layers.vswr` support `setVisible`,
341
372
  `setStyle({ stroke?, strokeWidth? })`, `addValue`, and `removeValue`.
@@ -345,8 +376,23 @@ Duplicate additions and removal of absent values have no effect.
345
376
 
346
377
  Grid labels use normalized values; readings use physical units. Strokes default
347
378
  to slate gray with 1 px major and 0.6 px minor lines. Stroke widths stay constant
348
- under zoom; label sizes use SVG user units. Layer controls do not expose drawing
349
- objects or SVG nodes.
379
+ under zoom. Layer controls do not expose drawing objects or SVG nodes.
380
+
381
+ Grid labels adapt automatically to the chart's rendered size,
382
+ including static charts with zoom disabled. The layout prioritizes key values,
383
+ hides labels that overlap or extend beyond the default view, and prioritizes zero
384
+ and the highest labeled axis value. It recalculates on resize and layer visibility
385
+ or style changes. Zoom and pan preserve the chosen labels and scale them smoothly
386
+ with the grid. This changes only labels; grid lines and ticks remain unchanged.
387
+ SVG exports preserve the current label layout.
388
+
389
+ For small static charts, hide minor grid lines and, when unnecessary, peripheral
390
+ scales using the controls above. Very small charts necessarily show fewer values;
391
+ use a larger container for detailed readings. Peripheral labels retain their full
392
+ wording and scale with the chart. Their rulers form two outlined bands: phase
393
+ (transmission/reflection) and electrical length (toward load/generator), with a
394
+ shared tick circle inside each band.
395
+ The independent `SmithScales` panel uses its own responsive layout.
350
396
 
351
397
  ## Parameter scales
352
398
 
@@ -404,12 +450,12 @@ result rejects the operation without changing Z₀ or data. Active marker drags
404
450
  cursor indicators are cleared, and marker readings are queued with the new values.
405
451
  Changing to the current Z₀ has no effect. Mutations after destruction throw.
406
452
 
407
- Two DOM-independent helpers are also exported:
453
+ Two DOM-independent methods are available on `RfCalculations`:
408
454
 
409
- - `renormalizeReflection(gamma, fromOhms, toOhms)` returns the new complex Γ or
455
+ - `RfCalculations.renormalizeReflection(gamma, fromOhms, toOhms)` returns the new complex Γ or
410
456
  `undefined` for a singular/unrepresentable result. Exact open and short limits
411
457
  remain Γ = +1 and −1. References must be positive, finite, real ohm values.
412
- - `renormalizeSamples(samples, fromOhms, toOhms)` returns a new sample array in
458
+ - `RfCalculations.renormalizeSamples(samples, fromOhms, toOhms)` returns a new sample array in
413
459
  the same order, preserving frequencies. Invalid samples or a singular result
414
460
  throw `RangeError`; the input is never modified.
415
461
 
@@ -424,15 +470,15 @@ multiport renormalization are outside the supported scope.
424
470
 
425
471
  ## Touchstone import
426
472
 
427
- `parseTouchstone(text)` returns `{ samples, referenceImpedanceOhms }`. It accepts
473
+ `Touchstone.parse(text)` returns `{ samples, referenceImpedanceOhms }`. It accepts
428
474
  one-port Touchstone 1.x, converts RI/MA/DB into Cartesian Γ and all frequencies
429
475
  into Hz, and throws for unsupported or invalid input.
430
476
 
431
477
  ```ts
432
- import { Smith, parseTouchstone } from 'smithkit';
478
+ import { Smith, Touchstone } from 'smithkit';
433
479
 
434
480
  async function showMeasurement(file: File): Promise<Smith> {
435
- const { samples, referenceImpedanceOhms } = parseTouchstone(await file.text());
481
+ const { samples, referenceImpedanceOhms } = Touchstone.parse(await file.text());
436
482
  const chart = new Smith(referenceImpedanceOhms);
437
483
  chart.draw('#smith');
438
484
  chart.addTrace(samples, { name: file.name });
@@ -441,10 +487,43 @@ async function showMeasurement(file: File): Promise<Smith> {
441
487
  ```
442
488
 
443
489
  Omitted options use Touchstone defaults: GHz, S, MA, 50 Ω. For an existing chart,
444
- use `renormalizeSamples` when the file reference differs from
490
+ use `RfCalculations.renormalizeSamples` when the file reference differs from
445
491
  `chart.referenceImpedanceOhms`. The parser itself does not renormalize data.
446
492
  Multiport data and Touchstone 2.x are not supported.
447
493
 
494
+ ## SVG export
495
+
496
+ `chart.toSvg()` and `scales.toSvg()` return SVG strings. Call them after `draw()`
497
+ while the component is mounted and has a non-zero rendered size; exporting before
498
+ mounting, from a `display: none` container, or after destruction throws an error.
499
+
500
+ ```ts
501
+ const svg = chart.toSvg();
502
+ const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
503
+ const url = URL.createObjectURL(blob);
504
+ const link = document.createElement('a');
505
+ link.href = url;
506
+ link.download = 'smith-chart.svg';
507
+ document.body.appendChild(link);
508
+ link.click();
509
+ link.remove();
510
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
511
+ ```
512
+
513
+ The chart export preserves the current zoom/pan, layer visibility, labels, traces,
514
+ markers, and visible indicators. It captures the rendered points, including dense
515
+ trace point reduction; it is an image export, not a measurement-data export.
516
+ The scale export combines all twelve axes into one SVG using their current layout
517
+ and displayed readings. To export a marker reading, call `scales.update()` with
518
+ that marker's reflection coefficient before `scales.toSvg()`.
519
+
520
+ Files have explicit pixel dimensions and a transparent background. Presentation
521
+ styles are inlined so the application's stylesheet is not required. Text remains
522
+ editable, and curved captions retain their internal path references. Font files
523
+ and surrounding HTML controls/backgrounds are not embedded. Downloading is the
524
+ consumer's responsibility; the library does not create files or modify live nodes.
525
+ The demo provides separate **Export chart SVG** and **Export scales SVG** buttons.
526
+
448
527
  ## Development and license
449
528
 
450
529
  See [CONTRIBUTING.md](CONTRIBUTING.md) for development, testing, release, and demo
@@ -0,0 +1,6 @@
1
+ import type { MarkerSnapshot, MarkerComparison } from './measurements.js';
2
+ export declare class MarkerMeasurements {
3
+ private constructor();
4
+ /** Compare B against A without a chart or browser. Frequencies must be finite and non-negative. */
5
+ static compare(a: Pick<MarkerSnapshot, 'frequencyHz' | 'impedanceOhms' | 'reflectionCoefficient'>, b: Pick<MarkerSnapshot, 'frequencyHz' | 'impedanceOhms' | 'reflectionCoefficient'>): MarkerComparison;
6
+ }
@@ -0,0 +1,22 @@
1
+ export class MarkerMeasurements {
2
+ constructor() { }
3
+ /** Compare B against A without a chart or browser. Frequencies must be finite and non-negative. */
4
+ static compare(a, b) {
5
+ if (![a.frequencyHz, b.frequencyHz].every((frequencyHz) => Number.isFinite(frequencyHz) && frequencyHz >= 0)) {
6
+ throw new RangeError('Marker frequencies must be finite and non-negative.');
7
+ }
8
+ const isFinite = (value) => Number.isFinite(value.re) && Number.isFinite(value.im);
9
+ const hasPhase = (gamma) => isFinite(gamma) && (gamma.re !== 0 || gamma.im !== 0);
10
+ const phaseDelta = hasPhase(a.reflectionCoefficient) && hasPhase(b.reflectionCoefficient)
11
+ ? ((b.reflectionCoefficient.arg() - a.reflectionCoefficient.arg()) * 180) / Math.PI
12
+ : undefined;
13
+ const impedanceDelta = a.impedanceOhms && b.impedanceOhms && isFinite(a.impedanceOhms) && isFinite(b.impedanceOhms)
14
+ ? b.impedanceOhms.sub(a.impedanceOhms)
15
+ : undefined;
16
+ return {
17
+ frequencyDeltaHz: b.frequencyHz - a.frequencyHz,
18
+ impedanceDeltaOhms: impedanceDelta && isFinite(impedanceDelta) ? impedanceDelta : undefined,
19
+ phaseDeltaDegrees: phaseDelta === undefined ? undefined : ((phaseDelta + 540) % 360) - 180,
20
+ };
21
+ }
22
+ }
@@ -1,10 +1,11 @@
1
1
  import { TraceSamples } from './samples.js';
2
2
  import type { ChartLayers, PeripheralScales } from './layers.js';
3
- import type { SmithReading } from './rf.js';
3
+ import type { SmithReading } from './rf/RfCalculations.js';
4
4
  import type { TraceOptions, TraceUpdateOptions, TraceInfo, MarkerSnapshot, MarkerComparison } from './measurements.js';
5
5
  export declare enum SmithEventType {
6
6
  Cursor = "cursor",
7
7
  Marker = "marker",
8
+ MarkerSelect = "marker-select",
8
9
  MarkerDragStart = "marker-drag-start",
9
10
  MarkerDragEnd = "marker-drag-end"
10
11
  }
@@ -12,19 +13,21 @@ export type SmithEvent = {
12
13
  type: SmithEventType.Cursor;
13
14
  data: SmithReading | undefined;
14
15
  } | {
15
- type: SmithEventType.Marker | SmithEventType.MarkerDragStart | SmithEventType.MarkerDragEnd;
16
+ type: SmithEventType.Marker | SmithEventType.MarkerSelect | SmithEventType.MarkerDragStart | SmithEventType.MarkerDragEnd;
16
17
  data: MarkerSnapshot;
17
18
  };
18
19
  export declare class Smith {
19
20
  private referenceOhms;
20
21
  private calcs;
21
22
  private scalers;
23
+ private readonly defaultTransform;
22
24
  private transform;
23
25
  private zoomEnabled;
24
26
  private applyingView;
25
27
  private zoomBehavior;
26
28
  private svg;
27
29
  private resizeObserver;
30
+ private readonly labelLayout;
28
31
  private container;
29
32
  private dataContainer;
30
33
  private markerContainer;
@@ -52,6 +55,8 @@ export declare class Smith {
52
55
  readonly peripheralScales: PeripheralScales;
53
56
  constructor(referenceOhms?: number);
54
57
  draw(target: string | HTMLElement): void;
58
+ /** Export the current mounted view as standalone SVG with a transparent background. */
59
+ toSvg(): string;
55
60
  /** Remove this chart and release its event handlers. Safe to call more than once. */
56
61
  destroy(): void;
57
62
  private updateViewportScale;
@@ -85,6 +90,8 @@ export declare class Smith {
85
90
  /** Place a marker on an existing sample; no interpolation is performed. */
86
91
  addMarker(traceId: string, sampleIndex?: number): string | undefined;
87
92
  removeMarker(id: string): boolean;
93
+ /** Focus a mounted, visible marker for keyboard interaction. */
94
+ focusMarker(id: string): boolean;
88
95
  setMarkerSample(id: string, sampleIndex: number): boolean;
89
96
  /** Select the nearest measured frequency; ties choose the earliest input sample. */
90
97
  setMarkerFrequency(id: string, frequencyHz: number): boolean;
package/dist/lib/Smith.js CHANGED
@@ -1,29 +1,32 @@
1
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';
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';
19
+ import { Complex } from './math/Complex.js';
18
20
  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';
21
+ import { GridLayerControl } from './grid/GridLayerControl.js';
22
+ import { CircleLayerControl } from './grid/CircleLayerControl.js';
23
+ import { RfCalculations } from './rf/RfCalculations.js';
24
+ import { MarkerMeasurements } from './MarkerMeasurements.js';
23
25
  export var SmithEventType;
24
26
  (function (SmithEventType) {
25
27
  SmithEventType["Cursor"] = "cursor";
26
28
  SmithEventType["Marker"] = "marker";
29
+ SmithEventType["MarkerSelect"] = "marker-select";
27
30
  SmithEventType["MarkerDragStart"] = "marker-drag-start";
28
31
  SmithEventType["MarkerDragEnd"] = "marker-drag-end";
29
32
  })(SmithEventType || (SmithEventType = {}));
@@ -31,12 +34,14 @@ export class Smith {
31
34
  referenceOhms;
32
35
  calcs = new SmithConstantCircle();
33
36
  scalers;
37
+ defaultTransform = d3.zoomIdentity.translate(62.5, 62.5).scale(0.75);
34
38
  transform = d3.zoomIdentity;
35
39
  zoomEnabled = true;
36
40
  applyingView = false;
37
41
  zoomBehavior = d3.zoom();
38
42
  svg;
39
43
  resizeObserver;
44
+ labelLayout;
40
45
  container;
41
46
  dataContainer;
42
47
  markerContainer;
@@ -68,7 +73,7 @@ export class Smith {
68
73
  throw new RangeError('Reference impedance must be positive and finite.');
69
74
  }
70
75
  const viewBoxSize = 500;
71
- const gridData = SmithArcsDefs.getData();
76
+ const gridData = GridDefinitions.create();
72
77
  this.scalers = this.createScalers(viewBoxSize);
73
78
  this.svg = new SmithSvg(viewBoxSize);
74
79
  this.container = new SmithGroup();
@@ -122,7 +127,6 @@ export class Smith {
122
127
  this.container.append(this.reactanceAxis);
123
128
  this.container.append(cursorContainer);
124
129
  this.container.append(this.dataContainer);
125
- this.container.append(this.markerContainer);
126
130
  const labels = new SmithGroup().attr('data-layer', 'labels').attr('pointer-events', 'none');
127
131
  for (const [name, layer] of [
128
132
  ['conductance', this.constConductance],
@@ -134,14 +138,16 @@ export class Smith {
134
138
  }
135
139
  labels.append(this.peripheralScaleRenderer.labels);
136
140
  this.container.append(labels);
141
+ this.container.append(this.markerContainer);
142
+ this.labelLayout = new LabelLayout(this.svg.Node, labels.Node, this.defaultTransform);
137
143
  const assertAlive = () => this.assertAlive();
138
144
  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
+ 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),
145
151
  };
146
152
  this.peripheralScales = {
147
153
  setVisible: (visible) => {
@@ -152,6 +158,7 @@ export class Smith {
152
158
  else {
153
159
  this.peripheralScaleRenderer.hide();
154
160
  }
161
+ this.labelLayout.update(true);
155
162
  },
156
163
  update: (gamma) => {
157
164
  assertAlive();
@@ -171,6 +178,11 @@ export class Smith {
171
178
  host.appendChild(this.svg.Node);
172
179
  this.updateViewportScale();
173
180
  }
181
+ /** Export the current mounted view as standalone SVG with a transparent background. */
182
+ toSvg() {
183
+ this.assertAlive();
184
+ return SvgExporter.chart(this.svg.Node);
185
+ }
174
186
  /** Remove this chart and release its event handlers. Safe to call more than once. */
175
187
  destroy() {
176
188
  if (this.destroyed) {
@@ -196,6 +208,7 @@ export class Smith {
196
208
  const scale = Math.hypot(matrix.a, matrix.b);
197
209
  this.data.forEach((trace) => trace.setViewportScale(scale));
198
210
  }
211
+ this.labelLayout.update();
199
212
  }
200
213
  assertAlive() {
201
214
  if (this.destroyed) {
@@ -252,19 +265,22 @@ export class Smith {
252
265
  }
253
266
  /** Last cursor position. Use cursor events to detect pointer leave. */
254
267
  get cursorReading() {
255
- return readReflection(this.cursor.Position, this.referenceImpedanceOhms);
268
+ return RfCalculations.readReflection(this.cursor.Position, this.referenceImpedanceOhms);
256
269
  }
257
270
  initializeZoom() {
258
271
  const zoom = this.zoomBehavior
259
272
  .scaleExtent([0.6, 1000])
260
- .filter((event) => this.zoomEnabled && (!event.ctrlKey || event.type === 'wheel') && !event.button)
273
+ .filter((event) => this.zoomEnabled &&
274
+ this.draggedMarkers.size === 0 &&
275
+ (!event.ctrlKey || event.type === 'wheel') &&
276
+ !event.button)
261
277
  .on('start', (event) => {
262
278
  if (event.sourceEvent) {
263
279
  this.mouseGesture.capture(event.sourceEvent, 'zoom');
264
280
  }
265
281
  })
266
282
  .on('zoom', (event) => {
267
- if (!this.zoomEnabled && !this.applyingView) {
283
+ if ((!this.zoomEnabled || this.draggedMarkers.size > 0) && !this.applyingView) {
268
284
  // An already active mouse/touch gesture still needs its normal end event.
269
285
  // Restore D3's view through its public API while that gesture finishes.
270
286
  this.applyView(this.transform);
@@ -300,7 +316,7 @@ export class Smith {
300
316
  }
301
317
  resetView() {
302
318
  this.assertAlive();
303
- const transform = d3.zoomIdentity.translate(62.5, 62.5).scale(0.75);
319
+ const transform = this.defaultTransform;
304
320
  this.applyView(transform);
305
321
  }
306
322
  onZoom(transform) {
@@ -411,6 +427,7 @@ export class Smith {
411
427
  data.setStyle(options);
412
428
  if (options.name !== undefined) {
413
429
  this.traceMetadata.get(data).name = options.name.trim();
430
+ data.setName(options.name.trim());
414
431
  }
415
432
  if (options.color !== undefined) {
416
433
  data.setColor(options.color);
@@ -459,6 +476,14 @@ export class Smith {
459
476
  const location = this.findMarker(id);
460
477
  return location ? this.data[location.datasetNo].removeMarker(location.markerNo) : false;
461
478
  }
479
+ /** Focus a mounted, visible marker for keyboard interaction. */
480
+ focusMarker(id) {
481
+ this.assertAlive();
482
+ const location = this.findMarker(id);
483
+ return location
484
+ ? this.data[location.datasetNo].Markers[location.markerNo].marker.focus()
485
+ : false;
486
+ }
462
487
  setMarkerSample(id, sampleIndex) {
463
488
  this.assertAlive();
464
489
  const location = this.findMarker(id);
@@ -482,7 +507,7 @@ export class Smith {
482
507
  const data = this.data[location.datasetNo];
483
508
  const marker = data.Markers[location.markerNo];
484
509
  return {
485
- ...readReflection(Complex.from(...marker.selectedPoint.reflectionCoefficient), this.referenceImpedanceOhms),
510
+ ...RfCalculations.readReflection(Complex.from(...marker.selectedPoint.reflectionCoefficient), this.referenceImpedanceOhms),
486
511
  frequencyHz: marker.selectedPoint.frequencyHz,
487
512
  traceId: this.traceMetadata.get(data).id,
488
513
  markerId: id,
@@ -494,7 +519,7 @@ export class Smith {
494
519
  compareMarkers(a, b) {
495
520
  const first = this.getMarker(a);
496
521
  const second = this.getMarker(b);
497
- return first && second ? compareMarkerReadings(first, second) : undefined;
522
+ return first && second ? MarkerMeasurements.compare(first, second) : undefined;
498
523
  }
499
524
  /** Current positive real reference impedance, in ohms. */
500
525
  get referenceImpedanceOhms() {
@@ -504,11 +529,11 @@ export class Smith {
504
529
  renormalize(referenceImpedanceOhms) {
505
530
  this.assertAlive();
506
531
  // Validate even with no traces, and prepare every result before changing any state.
507
- renormalizeSamples([], this.referenceOhms, referenceImpedanceOhms);
532
+ RfCalculations.renormalizeSamples([], this.referenceOhms, referenceImpedanceOhms);
508
533
  if (referenceImpedanceOhms === this.referenceOhms) {
509
534
  return;
510
535
  }
511
- const samples = this.data.map((data) => renormalizeSamples(data.Samples, this.referenceOhms, referenceImpedanceOhms));
536
+ const samples = this.data.map((data) => RfCalculations.renormalizeSamples(data.Samples, this.referenceOhms, referenceImpedanceOhms));
512
537
  this.data.forEach((data) => data.Markers.forEach((entry) => entry.marker.cancelDrag()));
513
538
  this.referenceOhms = referenceImpedanceOhms;
514
539
  this.data.forEach((data, index) => data.update(samples[index], 'sample-index'));
@@ -534,6 +559,13 @@ export class Smith {
534
559
  }, this.markerContainer, options);
535
560
  const number = this.nextTraceId++;
536
561
  this.traceMetadata.set(data, { id: `trace-${number}`, name: `Trace ${number}` });
562
+ data.setName(`Trace ${number}`);
563
+ data.setMarkerSelectHandler((marker) => {
564
+ const snapshot = this.getMarker(this.markerId(marker));
565
+ if (snapshot) {
566
+ this.emit({ type: SmithEventType.MarkerSelect, data: snapshot });
567
+ }
568
+ });
537
569
  data.setMarkerMoveHandler((index) => {
538
570
  const marker = data.Markers[index];
539
571
  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
+ }