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/README.md CHANGED
@@ -5,17 +5,26 @@ 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.2.x? Version 0.3.0 replaces `GridLayer.setMinorVisible()` with
9
+ `setDetail('basic' | 'standard' | 'detailed')`. Replace `false` with `'standard'`
10
+ and `true` with `'detailed'`; `'basic'` shows only the principal circles.
11
+
12
+ Upgrading from 0.1.x? Version 0.2.0 groups calculation, parsing, formatting, and
13
+ comparison functions into classes. See the [migration guide](https://github.com/rosek86/smithkit/blob/v0.2.0/docs/migration-0.2.md)
14
+ for the changed imports.
15
+
8
16
  ## Features
9
17
 
10
18
  - Labeled resistance, reactance, conductance, and susceptance grids.
11
19
  - Configurable constant-Q and constant-VSWR circles.
12
20
  - Zoom, pan, view reset, and cursor readings.
13
- - Named traces with colors, visibility, and draggable markers that snap to samples.
21
+ - Named traces with colors, visibility, and keyboard/touch/mouse markers that snap to samples.
14
22
  - Marker comparisons: frequency, complex impedance, and wrapped phase differences.
15
23
  - Physical impedance/admittance conversions and DOM-independent RF readings.
16
24
  - Twelve parameter scales mounted independently of chart zoom.
17
25
  - Four peripheral rulers for phase and electrical length.
18
26
  - One-port Touchstone 1.x import with RI, MA, and DB representations.
27
+ - Standalone SVG and configurable PNG export of the chart and radial scales.
19
28
  - ESM modules and TypeScript declarations.
20
29
 
21
30
  ## Installation
@@ -63,11 +72,11 @@ must be finite and non-negative; both coordinates must be finite. A trace must
63
72
  contain at least one sample.
64
73
 
65
74
  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.
75
+ samples with a mouse or touch, scroll to zoom, and drag the chart to pan. Markers
76
+ and their focus indicators stay above grid labels and sample points. `chart.resetView()` restores the initial view, including peripheral rulers.
68
77
 
69
78
  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.
79
+ the package and using its calculation or parsing methods does not require a DOM.
71
80
 
72
81
  Zoom is enabled by default. For a fixed view, call `chart.setZoomEnabled(false)` before or after mounting.
73
82
  It disables wheel/double-click zoom and mouse/touch panning and pinch zoom while
@@ -89,6 +98,25 @@ chart.draw('#smith');
89
98
  container throws. Call `chart.destroy()` on unmount; it removes the SVG, releases
90
99
  event handlers, and cancels queued notifications. Repeated destruction is safe.
91
100
 
101
+ ### Keyboard and touch markers
102
+
103
+ Use Tab/Shift+Tab to focus a marker. Arrow keys move through samples in input order;
104
+ Right/Up advances and Left/Down goes back. Shift+Arrow or Page Up/Down moves ten
105
+ samples, and Home/End selects the first/last sample. Navigation clamps at the ends
106
+ and also works when chart zoom is disabled. A single-sample trace remains focusable
107
+ for reading, with its slider marked disabled.
108
+
109
+ A focused marker has a contrasting outline and exposes its trace name, marker
110
+ number, sample position, and frequency to assistive technology. Hidden traces are
111
+ excluded from keyboard focus. `chart.focusMarker(id)` allows a separate marker
112
+ selector to focus the matching chart control. The demo provides a **Focus on chart**
113
+ button beside its marker selector.
114
+
115
+ Markers have a 44 CSS px circular hit area and an 18 CSS px triangle, kept constant
116
+ through zoom and container resizing. A marker touch gesture does not pan the chart;
117
+ cancelling it releases the drag state. Touch target areas may overlap for nearby
118
+ markers; use keyboard focus or the demo selector to reach an obscured marker.
119
+
92
120
  ## Traces and markers
93
121
 
94
122
  Trace and marker IDs are opaque strings, unique within a chart and never reused
@@ -121,6 +149,7 @@ chart.removeMarker(markerB);
121
149
  | `clearTraces()` | Removes all traces and their markers. |
122
150
  | `addMarker(traceId, sampleIndex = 0)` | Returns a marker ID, or `undefined` if the trace is missing. |
123
151
  | `getMarker(id)` | Returns a `MarkerSnapshot`, or `undefined` if missing. |
152
+ | `focusMarker(id)` | Moves keyboard focus to a mounted, visible marker; returns `false` if missing or not focusable. |
124
153
  | `setMarkerSample(id, sampleIndex)` | Selects an existing sample and queues a marker notification. |
125
154
  | `removeMarker(id)` | Removes one marker without renumbering the remaining markers. |
126
155
  | `compareMarkers(a, b)` | Returns B − A, or `undefined` if either marker is missing. |
@@ -192,7 +221,7 @@ const unsubscribe = chart.onEvent((event) => {
192
221
  console.log(event.data?.impedanceOhms?.toVector());
193
222
  return;
194
223
  }
195
- // Marker, MarkerDragStart, and MarkerDragEnd always carry a MarkerSnapshot.
224
+ // Marker, MarkerSelect, MarkerDragStart, and MarkerDragEnd carry a MarkerSnapshot.
196
225
  console.log(event.data.markerId, event.data.frequencyHz);
197
226
  });
198
227
 
@@ -202,7 +231,12 @@ chart.destroy();
202
231
  ```
203
232
 
204
233
  The event types are `Cursor` (`'cursor'`), `Marker` (`'marker'`),
234
+ `MarkerSelect` (`'marker-select'`),
205
235
  `MarkerDragStart` (`'marker-drag-start'`), and `MarkerDragEnd` (`'marker-drag-end'`).
236
+ `MarkerSelect` fires synchronously when a marker receives focus or is engaged by
237
+ keyboard navigation or a pointer press. Use it to select the corresponding
238
+ readout; it does not indicate a position change. Keyboard changes use the normal
239
+ queued `Marker` event and do not emit drag lifecycle events.
206
240
  Cursor movement is suspended during marker dragging. Drag start/end events fire
207
241
  synchronously; marker position notifications are queued and report the latest
208
242
  snapshot at delivery. Before delivery, repeated updates to the same marker coalesce
@@ -213,7 +247,7 @@ Trace metadata changes do not emit marker events; refresh metadata after mutatio
213
247
  Destruction suppresses application callbacks.
214
248
 
215
249
  `chart.cursorReading` returns the last cursor position. Cursor and marker readings
216
- share `SmithReading`; `readReflection(gamma, referenceImpedanceOhms = 50)` produces
250
+ share `SmithReading`; `RfCalculations.readReflection(gamma, referenceImpedanceOhms = 50)` produces
217
251
  the same reading without constructing a chart.
218
252
 
219
253
  | Fields | Units / meaning |
@@ -239,7 +273,7 @@ infinite scalar limit is preserved: perfect match gives infinite return loss;
239
273
  For active loads (\|Γ\| > 1), VSWR, standing-wave quantities, reflection loss,
240
274
  attenuation, and power transmission are `undefined`. Other quantities remain
241
275
  available. Non-finite complex inputs or invalid reference impedances throw
242
- `RangeError` in the public RF functions. Unrepresentable complex conversion
276
+ `RangeError` in the public RF methods. Unrepresentable complex conversion
243
277
  results are `undefined`.
244
278
 
245
279
  ## Selecting a marker by frequency
@@ -259,22 +293,22 @@ measurement frequency; dragging and sample selection keep that field synchronize
259
293
  ## Calculations without a chart
260
294
 
261
295
  ```ts
262
- import { Complex, readReflection, impedanceToReflection } from 'smithkit';
296
+ import { Complex, RfCalculations } from 'smithkit';
263
297
 
264
- const gamma = impedanceToReflection(Complex.from(75, 25), 50)!;
265
- const reading = readReflection(gamma, 50);
298
+ const gamma = RfCalculations.impedanceToReflection(Complex.from(75, 25), 50)!;
299
+ const reading = RfCalculations.readReflection(gamma, 50);
266
300
  console.log(reading.impedanceOhms?.toVector()); // Approximately [75, 25].
267
301
  console.log(reading.admittanceSiemens?.toVector());
268
302
  ```
269
303
 
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`. |
304
+ | Static method | Input and output |
305
+ | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
306
+ | `RfCalculations.reflectionToImpedance(gamma, referenceImpedanceOhms = 50)` | Γ → physical Z in Ω. |
307
+ | `RfCalculations.impedanceToReflection(impedanceOhms, referenceImpedanceOhms = 50)` | Physical Z in Ω → Γ. |
308
+ | `RfCalculations.reflectionToAdmittance(gamma, referenceImpedanceOhms = 50)` | Γ → physical Y in S. |
309
+ | `RfCalculations.admittanceToReflection(admittanceSiemens, referenceImpedanceOhms = 50)` | Physical Y in S → Γ. |
310
+ | `RfCalculations.readReflection(gamma, referenceImpedanceOhms = 50)` | Complete `SmithReading`. |
311
+ | `MarkerMeasurements.compare(a, b)` | Compare readings without a chart. Each input needs `frequencyHz`, `reflectionCoefficient`, and `impedanceOhms`. |
278
312
 
279
313
  Comparison results contain `frequencyDeltaHz`, `impedanceDeltaOhms`, and
280
314
  `phaseDeltaDegrees`. Phase differences use B − A wrapped to **[−180°, 180°)**:
@@ -285,27 +319,27 @@ a point comparison, not sweep phase unwrapping or group delay.
285
319
 
286
320
  `Complex.from(re, im)` constructs complex numbers. Read components through `re` and `im`. Arithmetic (`add`, `sub`,
287
321
  `mul`, `div`, `pow`) returns new values. The class also provides `abs`, `arg`
288
- (radians), and static functions such as `Complex.conj`, `Complex.sqrt`,
322
+ (radians), and static methods such as `Complex.conj`, `Complex.sqrt`,
289
323
  `Complex.exp`, `Complex.log`, and trigonometric/hyperbolic functions and inverses. `toVector()` returns `[real, imaginary]`; `toString(dp)` formats
290
324
  a Cartesian value. See the exported declarations for all operations.
291
325
 
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
326
+ The `SmithFormatter` class provides static methods: `SmithFormatter.number(value)` (three significant
327
+ digits and SI prefixes), `SmithFormatter.complex(value, unit?, decimalPlaces?)`, and
328
+ `SmithFormatter.polar(value, unit?, decimalPlaces?)` (phase in degrees). They work
295
329
  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'`.
330
+ `SmithFormatter.number` includes spacing so you can append a unit, e.g. `SmithFormatter.number(1e9) + 'Hz'`.
297
331
 
298
- `reactanceToComponent(reactanceOhms, frequencyHz)` calculates an ideal equivalent
332
+ `RfCalculations.reactanceToComponent(reactanceOhms, frequencyHz)` calculates an ideal equivalent
299
333
  series component without formatting or constructing a chart:
300
334
 
301
335
  ```ts
302
- import { reactanceToComponent, formatNumber } from 'smithkit';
336
+ import { RfCalculations, SmithFormatter } from 'smithkit';
303
337
 
304
- const component = reactanceToComponent(-50, 1e9);
338
+ const component = RfCalculations.reactanceToComponent(-50, 1e9);
305
339
  if (component?.kind === 'capacitor') {
306
- console.log(formatNumber(component.capacitanceFarads) + 'F'); // 3.18 pF
340
+ console.log(SmithFormatter.number(component.capacitanceFarads) + 'F'); // 3.18 pF
307
341
  } else if (component?.kind === 'inductor') {
308
- console.log(formatNumber(component.inductanceHenries) + 'H');
342
+ console.log(SmithFormatter.number(component.inductanceHenries) + 'H');
309
343
  }
310
344
  ```
311
345
 
@@ -314,6 +348,69 @@ Zero frequency, zero reactance, and unrepresentable results return `undefined`.
314
348
  Non-finite reactance or negative/non-finite frequency throws `RangeError`.
315
349
  This is a single-frequency equivalence, not a fitted circuit model.
316
350
 
351
+ ## Appearance and themes
352
+
353
+ `Smith` and `SmithScales` accept the same appearance configuration. The default
354
+ is the light preset with a transparent background. The dark preset includes a
355
+ solid dark background so its labels remain readable when embedded or exported.
356
+
357
+ ```ts
358
+ import { Smith, SmithScales } from 'smithkit';
359
+ import type { SmithAppearance } from 'smithkit';
360
+
361
+ const appearance: SmithAppearance = {
362
+ theme: 'dark',
363
+ overrides: {
364
+ fontFamily: 'Arial, sans-serif',
365
+ grid: { majorWidth: 1.2 },
366
+ cursor: { impedanceColor: '#fbbf24' },
367
+ marker: { focusColor: '#38bdf8' },
368
+ traceColors: ['#38bdf8', '#fb923c', '#a78bfa'],
369
+ },
370
+ };
371
+ const chart = new Smith(50, appearance);
372
+ const scales = new SmithScales(appearance);
373
+ chart.draw('#smith');
374
+ scales.draw('#scales');
375
+
376
+ // Update both independent components using the same configuration.
377
+ chart.setAppearance({ theme: 'light' });
378
+ scales.setAppearance({ theme: 'light' });
379
+ ```
380
+
381
+ Each `setAppearance` call replaces the previous preset and overrides; omitted
382
+ settings return to the selected preset, or light when `theme` is omitted.
383
+ Nested overrides are partial. Appearance is scoped to each component and does not
384
+ change the surrounding application's CSS or other chart instances.
385
+
386
+ | Override | Settings |
387
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
388
+ | `background`, `fontFamily` | Component background and shared font family. |
389
+ | `grid` | `stroke`, `textColor`, `majorWidth`, `minorWidth`, `fontSize`. |
390
+ | `boundary` | `stroke`, `width` for the Smith chart boundary. |
391
+ | `circles` | `q`, `vswr` colors and shared `width`. |
392
+ | `scales` | `stroke`, `boundaryColor`, `textColor`, `valueColor`, `indicatorColor`, `indicatorOutline`; `fontSize` for radial ticks, `titleFontSize`, `valueFontSize`, and `peripheralFontSize`. |
393
+ | `cursor` | `pointColor`, `impedanceColor`, `admittanceColor`, `pointSize` (dot diameter), `lineWidth`. |
394
+ | `marker` | `outlineColor`, `textColor`, `focusColor`, `focusHaloColor`. Marker fill follows its trace color. |
395
+ | `traceColors` | Non-empty palette for automatically colored traces. |
396
+
397
+ Colors must be solid CSS colors accepted by the library's color parser, including
398
+ hex, RGB, HSL, named colors, and `transparent`; CSS variable references are not
399
+ accepted in the configuration. Numeric values must be positive and finite.
400
+ Line widths and cursor point size are CSS pixels. Font sizes are SVG chart/scale
401
+ units and follow the existing layout; grid labels retain their adaptive minimum
402
+ size. Marker geometry and the 44 px touch target stay unchanged.
403
+
404
+ Appearance updates retain zoom, samples, marker selection/focus, layer visibility,
405
+ and scale readings. They recolor existing and future automatically colored traces.
406
+ Explicit colors passed through trace options and explicit layer styles take
407
+ precedence and survive theme changes. Applying a new appearance does not reset
408
+ those individual overrides. Invalid appearance settings throw before making changes.
409
+
410
+ SVG exports resolve the appearance without requiring theme CSS. A configured
411
+ background is included in the exported image; `transparent` preserves transparency.
412
+ The **Theme** selector in the demo's **Chart settings** dialog updates both library components and the demo UI.
413
+
317
414
  ## Grid layers
318
415
 
319
416
  Resistance and reactance are initially visible. Conductance, susceptance, Q,
@@ -324,18 +421,38 @@ chart.layers.resistance.setVisible(false);
324
421
  chart.layers.reactance.setVisible(false);
325
422
  chart.layers.conductance.setVisible(true);
326
423
  chart.layers.susceptance.setVisible(true);
327
- chart.layers.conductance.setMinorVisible(false);
424
+ chart.layers.conductance.setDetail('standard');
425
+ chart.layers.conductance.setLabelsVisible(false);
426
+ chart.layers.susceptance.setLabelsVisible(false);
328
427
  chart.layers.susceptance.setStyle({ stroke: '#64748b', textColor: '#2a7f62' });
329
428
  chart.layers.vswr.addValue(4);
330
429
  chart.layers.vswr.setVisible(true);
331
430
  ```
332
431
 
333
- The four grid layers support `setVisible`, `setMinorVisible`, and
432
+ The four grid layers support `setVisible`, `setLabelsVisible`, `setDetail`, and
334
433
  `setStyle(Partial<GridStyle>)`. Style fields: `stroke`, `majorWidth`, `minorWidth`,
335
434
  `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.
435
+ in CSS pixels. Font size is a positive finite base size in chart units; adaptive
436
+ layout raises it when needed to keep the font size at least 9 CSS pixels in the
437
+ default view. Partial updates retain other settings. Invalid numeric styles throw
438
+ before any property is changed.
439
+
440
+ Choose `setDetail('basic' | 'standard' | 'detailed')` independently for each grid layer:
441
+
442
+ - **Basic:** complete circles and arcs for normalized values 0.2, 0.5, 1, 2, and 5,
443
+ with matching axis/rim labels and the zero label. Intended for small or static charts.
444
+ - **Standard:** the full major grid and its labels, without minor lines.
445
+ - **Detailed** (default): the full major and minor grid.
446
+
447
+ Changing detail preserves layer visibility, styles, and the current view. Label
448
+ collision handling still applies at every level. Peripheral scales and Q/VSWR
449
+ circles are independent. The demo's **Chart settings → Grid detail** selector updates all four grid layers.
450
+
451
+ Use `setLabelsVisible(false)` to hide a grid layer's labels while keeping its lines.
452
+ Labels default to visible. The setting persists through detail, style, theme, and
453
+ whole-layer visibility changes and is included in SVG exports. Calling
454
+ `setLabelsVisible(true)` restores labels allowed by the current detail and collision
455
+ layout; it does not show a hidden layer. The **Grid labels** checkbox in the demo’s **Chart settings** dialog controls all four grid layers together. Peripheral scale labels are unaffected.
339
456
 
340
457
  `layers.q` and `layers.vswr` support `setVisible`,
341
458
  `setStyle({ stroke?, strokeWidth? })`, `addValue`, and `removeValue`.
@@ -345,8 +462,23 @@ Duplicate additions and removal of absent values have no effect.
345
462
 
346
463
  Grid labels use normalized values; readings use physical units. Strokes default
347
464
  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.
465
+ under zoom. Layer controls do not expose drawing objects or SVG nodes.
466
+
467
+ Grid labels adapt automatically to the chart's rendered size,
468
+ including static charts with zoom disabled. The layout prioritizes key values,
469
+ hides labels that overlap or extend beyond the default view, and prioritizes zero
470
+ and the highest labeled axis value. It recalculates on resize and layer visibility
471
+ or style changes. Zoom and pan preserve the chosen labels and scale them smoothly
472
+ with the grid. This changes only labels; grid lines and ticks remain unchanged.
473
+ SVG exports preserve the current label layout.
474
+
475
+ For small static charts, hide minor grid lines and, when unnecessary, peripheral
476
+ scales using the controls above. Very small charts necessarily show fewer values;
477
+ use a larger container for detailed readings. Peripheral labels retain their full
478
+ wording and scale with the chart. Their rulers form two outlined bands: phase
479
+ (transmission/reflection) and electrical length (toward load/generator), with a
480
+ shared tick circle inside each band.
481
+ The independent `SmithScales` panel uses its own responsive layout.
350
482
 
351
483
  ## Parameter scales
352
484
 
@@ -404,12 +536,12 @@ result rejects the operation without changing Z₀ or data. Active marker drags
404
536
  cursor indicators are cleared, and marker readings are queued with the new values.
405
537
  Changing to the current Z₀ has no effect. Mutations after destruction throw.
406
538
 
407
- Two DOM-independent helpers are also exported:
539
+ Two DOM-independent methods are available on `RfCalculations`:
408
540
 
409
- - `renormalizeReflection(gamma, fromOhms, toOhms)` returns the new complex Γ or
541
+ - `RfCalculations.renormalizeReflection(gamma, fromOhms, toOhms)` returns the new complex Γ or
410
542
  `undefined` for a singular/unrepresentable result. Exact open and short limits
411
543
  remain Γ = +1 and −1. References must be positive, finite, real ohm values.
412
- - `renormalizeSamples(samples, fromOhms, toOhms)` returns a new sample array in
544
+ - `RfCalculations.renormalizeSamples(samples, fromOhms, toOhms)` returns a new sample array in
413
545
  the same order, preserving frequencies. Invalid samples or a singular result
414
546
  throw `RangeError`; the input is never modified.
415
547
 
@@ -424,15 +556,15 @@ multiport renormalization are outside the supported scope.
424
556
 
425
557
  ## Touchstone import
426
558
 
427
- `parseTouchstone(text)` returns `{ samples, referenceImpedanceOhms }`. It accepts
559
+ `Touchstone.parse(text)` returns `{ samples, referenceImpedanceOhms }`. It accepts
428
560
  one-port Touchstone 1.x, converts RI/MA/DB into Cartesian Γ and all frequencies
429
561
  into Hz, and throws for unsupported or invalid input.
430
562
 
431
563
  ```ts
432
- import { Smith, parseTouchstone } from 'smithkit';
564
+ import { Smith, Touchstone } from 'smithkit';
433
565
 
434
566
  async function showMeasurement(file: File): Promise<Smith> {
435
- const { samples, referenceImpedanceOhms } = parseTouchstone(await file.text());
567
+ const { samples, referenceImpedanceOhms } = Touchstone.parse(await file.text());
436
568
  const chart = new Smith(referenceImpedanceOhms);
437
569
  chart.draw('#smith');
438
570
  chart.addTrace(samples, { name: file.name });
@@ -441,10 +573,127 @@ async function showMeasurement(file: File): Promise<Smith> {
441
573
  ```
442
574
 
443
575
  Omitted options use Touchstone defaults: GHz, S, MA, 50 Ω. For an existing chart,
444
- use `renormalizeSamples` when the file reference differs from
576
+ use `RfCalculations.renormalizeSamples` when the file reference differs from
445
577
  `chart.referenceImpedanceOhms`. The parser itself does not renormalize data.
446
578
  Multiport data and Touchstone 2.x are not supported.
447
579
 
580
+ ## PNG and report export
581
+
582
+ `chart.toPng(options?)` and `scales.toPng(options?)` return `Promise<Blob>` with MIME
583
+ type `image/png`. Call them in a browser after mounting the components at non-zero
584
+ sizes, as with SVG export. The current view, labels, colors, markers, and scale
585
+ readings are captured without modifying the live components.
586
+
587
+ ```ts
588
+ import { SmithScales } from 'smithkit';
589
+
590
+ const scales = new SmithScales();
591
+ scales.draw('#scales');
592
+ const png = await chart.toPng({ width: 3200, background: 'white', legend: true });
593
+ // Use the Blob with your application's download, preview, or upload flow.
594
+ const report = await chart.toPng({ width: 2400, scales, legend: true });
595
+ const scalePng = await scales.toPng({ width: 1200, background: 'transparent' });
596
+ ```
597
+
598
+ - `width` / `height`: output pixels. Omit both to use the snapshot's natural size;
599
+ specify one to preserve proportions. With both set, the complete image is
600
+ centered and fitted without distortion or cropping. Padding uses the chosen
601
+ background, or transparency when no background is specified.
602
+ - `background`: omit to keep component backgrounds, or pass a concrete CSS color
603
+ (for example `'#fff'`) or `'transparent'` to replace them. Chart colors stay as
604
+ configured; choose a suitable chart theme for your report background.
605
+ - `legend` (chart only): append the names and colors of visible traces. Long names
606
+ wrap; hidden traces are omitted. Default: `false`.
607
+ - `markerLegend` (chart only): independently append marker descriptions below the chart/scales.
608
+ Default: `false`. `true` includes every marker of visible traces, with trace name,
609
+ marker number, frequency, and impedance. Use an options object to select `markerIds`
610
+ and `fields` (`frequency`, `impedance`, `admittance`, `reflectionCoefficient`, `vswr`,
611
+ `returnLoss`). Fields use Hz with SI prefixes, Ω, mS, dimensionless Γ, VSWR ratios,
612
+ and dB respectively. Empty `markerIds` selects none; empty `fields` keeps only names
613
+ and marker numbers. Hidden traces are always omitted, duplicate selections appear
614
+ once, and unknown IDs/fields throw. Long descriptions wrap. Readings are captured at
615
+ export invocation, without moving markers or changing the live view.
616
+ - `scales` (chart only): a mounted `SmithScales` instance to place centered below the chart.
617
+ Its current responsive layout and readings are preserved. Use `scaleReadout` to
618
+ export a specific reading independently of the live view.
619
+
620
+ ```ts
621
+ // Works with toSvg() as well. Trace and marker legends are independent.
622
+ const png = await chart.toPng({
623
+ legend: false,
624
+ markerLegend: {
625
+ fields: ['frequency', 'impedance', 'vswr'],
626
+ // Omit markerIds to include all markers of visible traces.
627
+ },
628
+ });
629
+ ```
630
+
631
+ Dimensions must be positive integers, at most 8192 px per side and 32 megapixels
632
+ in total. Invalid options and unavailable/destroyed components reject the promise.
633
+ PNG export uses browser SVG decoding and Canvas 2D and releases temporary image URLs.
634
+ Fonts must be available to the browser; external font files are not embedded.
635
+ Export size increases resolution; it does not change the current label density.
636
+
637
+ The demo's **Export** dialog supports chart, scales, or a combined report,
638
+ custom dimensions/background, and independent trace and marker legends. **Include marker legend** reveals field and marker checkboxes; it is disabled for scales-only exports. The default output width is 3200 px. Choose PNG or SVG in the same dialog; both formats support these report options. The demo starts in dark mode. For exports containing parameter scales, **Scale readings** explicitly selects a marker or **None**, independently of the Cursor/Marker tab. Marker exports include the trace name, marker number, and frequency; cursor readings are not exported.
639
+
640
+ To export a specific scale reading without changing the live view, pass `readout` to
641
+ `scales.toSvg()` / `scales.toPng()`, or `scaleReadout` to a combined chart export:
642
+
643
+ ```ts
644
+ import { Complex, SmithScales } from 'smithkit';
645
+
646
+ const scales = new SmithScales();
647
+ scales.draw('#scales');
648
+ const report = await chart.toPng({
649
+ scales,
650
+ scaleReadout: {
651
+ reflectionCoefficient: Complex.from(0.2, 0.3),
652
+ label: 'Antenna · Marker 1 · Frequency: 1 GHz',
653
+ },
654
+ });
655
+ ```
656
+
657
+ Use `{ reflectionCoefficient: null }` for scales without readings. Omit the override to
658
+ preserve the current readings. Labels wrap to fit the exported scales.
659
+
660
+ ## SVG export
661
+
662
+ `chart.toSvg(options?)` and `scales.toSvg(options?)` return SVG strings synchronously.
663
+ They accept the same `SmithImageExportOptions` / `ImageExportOptions` as PNG export,
664
+ including size, background, legend, and scales. Omitting options preserves the
665
+ original standalone SVG snapshot. Call them after `draw()`
666
+ while the component is mounted and has a non-zero rendered size; exporting before
667
+ mounting, from a `display: none` container, or after destruction throws an error.
668
+
669
+ ```ts
670
+ const svg = chart.toSvg();
671
+ const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
672
+ const url = URL.createObjectURL(blob);
673
+ const link = document.createElement('a');
674
+ link.href = url;
675
+ link.download = 'smith-chart.svg';
676
+ document.body.appendChild(link);
677
+ link.click();
678
+ link.remove();
679
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
680
+ ```
681
+
682
+ The chart export preserves the current zoom/pan, layer visibility, labels, traces,
683
+ markers, and visible indicators. It captures the rendered points, including dense
684
+ trace point reduction; it is an image export, not a measurement-data export.
685
+ The scale export combines all twelve axes into one SVG using their current layout
686
+ and displayed readings. To export a marker reading, call `scales.update()` with
687
+ that marker's reflection coefficient before `scales.toSvg()`.
688
+
689
+ Files have explicit pixel dimensions and preserve the configured component background
690
+ (transparent by default). Presentation
691
+ styles are inlined so the application's stylesheet is not required. Text remains
692
+ editable, and curved captions retain their internal path references. Font files
693
+ and surrounding HTML controls/backgrounds are not embedded. Downloading is the
694
+ consumer's responsibility; the library does not create files or modify live nodes.
695
+ In the demo, choose **Export → Format → SVG**, then select chart, scales, or the combined report.
696
+
448
697
  ## Development and license
449
698
 
450
699
  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,13 @@
1
- import { TraceSamples } from './samples.js';
1
+ import type { SmithImageExportOptions } from './svg/export.js';
2
+ import type { SmithAppearance } from './appearance/types.js';
3
+ import type { TraceSamples } from './samples.js';
2
4
  import type { ChartLayers, PeripheralScales } from './layers.js';
3
- import type { SmithReading } from './rf.js';
5
+ import type { SmithReading } from './rf/RfCalculations.js';
4
6
  import type { TraceOptions, TraceUpdateOptions, TraceInfo, MarkerSnapshot, MarkerComparison } from './measurements.js';
5
7
  export declare enum SmithEventType {
6
8
  Cursor = "cursor",
7
9
  Marker = "marker",
10
+ MarkerSelect = "marker-select",
8
11
  MarkerDragStart = "marker-drag-start",
9
12
  MarkerDragEnd = "marker-drag-end"
10
13
  }
@@ -12,64 +15,42 @@ export type SmithEvent = {
12
15
  type: SmithEventType.Cursor;
13
16
  data: SmithReading | undefined;
14
17
  } | {
15
- type: SmithEventType.Marker | SmithEventType.MarkerDragStart | SmithEventType.MarkerDragEnd;
18
+ type: SmithEventType.Marker | SmithEventType.MarkerSelect | SmithEventType.MarkerDragStart | SmithEventType.MarkerDragEnd;
16
19
  data: MarkerSnapshot;
17
20
  };
18
21
  export declare class Smith {
19
22
  private referenceOhms;
20
- private calcs;
21
- private scalers;
22
- private transform;
23
- private zoomEnabled;
24
- private applyingView;
25
- private zoomBehavior;
26
- private svg;
27
- private resizeObserver;
28
- private container;
29
- private dataContainer;
30
- private markerContainer;
31
- private reactanceAxis;
32
- private constResistance;
33
- private constReactance;
34
- private constConductance;
35
- private constSusceptance;
36
- private constSwrCircles;
37
- private constQCircles;
38
- private cursor;
39
- private peripheralScaleRenderer;
23
+ private theme;
24
+ private readonly renderer;
40
25
  private data;
41
26
  private destroyed;
42
- private mouseGesture;
43
27
  private nextDatasetColor;
44
28
  private nextTraceId;
45
29
  private nextMarkerId;
46
30
  private traceMetadata;
47
31
  private markerIds;
48
32
  private draggedMarkers;
49
- private cursorBeforeMarkerDrag;
50
33
  private listeners;
51
34
  readonly layers: ChartLayers;
52
35
  readonly peripheralScales: PeripheralScales;
53
- constructor(referenceOhms?: number);
36
+ constructor(referenceOhms?: number, appearance?: SmithAppearance);
37
+ /** Replace the appearance while retaining explicit layer styles and trace colors. */
38
+ setAppearance(appearance: SmithAppearance): void;
54
39
  draw(target: string | HTMLElement): void;
40
+ /** Export the current mounted view as standalone SVG with its configured background. */
41
+ toSvg(options?: SmithImageExportOptions): string;
42
+ private exportLegend;
43
+ /** Export the mounted view as PNG, optionally with radial scales and a trace legend. */
44
+ toPng(options?: SmithImageExportOptions): Promise<Blob>;
55
45
  /** Remove this chart and release its event handlers. Safe to call more than once. */
56
46
  destroy(): void;
57
- private updateViewportScale;
58
47
  private assertAlive;
59
- private createScalers;
60
- private cursorMove;
61
48
  private markerDragChanged;
62
- private initCursor;
63
49
  /** Last cursor position. Use cursor events to detect pointer leave. */
64
50
  get cursorReading(): SmithReading;
65
- private initializeZoom;
66
51
  /** Enable or disable wheel, double-click, and mouse/touch zoom/pan. Keeps the current view. */
67
52
  setZoomEnabled(enabled: boolean): void;
68
- private applyView;
69
53
  resetView(): void;
70
- private onZoom;
71
- private cursorContainer;
72
- private drawReactanceAxis;
73
54
  /** Add a named trace with one initial marker. Returns a chart-local, stable ID. */
74
55
  addTrace(values: TraceSamples, options?: TraceOptions): string;
75
56
  /** Detached metadata snapshots; IDs and marker display numbers survive removals. */
@@ -85,6 +66,8 @@ export declare class Smith {
85
66
  /** Place a marker on an existing sample; no interpolation is performed. */
86
67
  addMarker(traceId: string, sampleIndex?: number): string | undefined;
87
68
  removeMarker(id: string): boolean;
69
+ /** Focus a mounted, visible marker for keyboard interaction. */
70
+ focusMarker(id: string): boolean;
88
71
  setMarkerSample(id: string, sampleIndex: number): boolean;
89
72
  /** Select the nearest measured frequency; ties choose the earliest input sample. */
90
73
  setMarkerFrequency(id: string, frequencyHz: number): boolean;