smithkit 0.3.0 → 0.5.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 (45) hide show
  1. package/README.md +324 -37
  2. package/dist/lib/OptionsValidation.d.ts +16 -0
  3. package/dist/lib/OptionsValidation.js +172 -0
  4. package/dist/lib/Smith.d.ts +20 -5
  5. package/dist/lib/Smith.js +114 -15
  6. package/dist/lib/appearance/Theme.js +5 -0
  7. package/dist/lib/grid/CircleLayerControl.d.ts +1 -1
  8. package/dist/lib/grid/CircleLayerControl.js +10 -8
  9. package/dist/lib/grid/ConstQCircles.d.ts +1 -0
  10. package/dist/lib/grid/ConstQCircles.js +5 -0
  11. package/dist/lib/grid/ConstSwrCircles.d.ts +1 -0
  12. package/dist/lib/grid/ConstSwrCircles.js +5 -0
  13. package/dist/lib/grid/GridLayerControl.js +5 -4
  14. package/dist/lib/grid/LayerControl.d.ts +1 -2
  15. package/dist/lib/grid/LayerControl.js +1 -10
  16. package/dist/lib/index.d.ts +3 -2
  17. package/dist/lib/io/Touchstone.d.ts +13 -7
  18. package/dist/lib/io/Touchstone.js +146 -46
  19. package/dist/lib/io/types.d.ts +17 -0
  20. package/dist/lib/io/types.js +1 -0
  21. package/dist/lib/layers.d.ts +7 -1
  22. package/dist/lib/measurements.d.ts +2 -0
  23. package/dist/lib/options.d.ts +58 -0
  24. package/dist/lib/options.js +1 -0
  25. package/dist/lib/rendering/SvgChartRenderer.d.ts +6 -3
  26. package/dist/lib/rendering/SvgChartRenderer.js +58 -11
  27. package/dist/lib/rf/RfCalculations.d.ts +3 -1
  28. package/dist/lib/rf/RfCalculations.js +23 -6
  29. package/dist/lib/samples.d.ts +5 -1
  30. package/dist/lib/scales/SmithPeripheralScales.d.ts +5 -0
  31. package/dist/lib/scales/SmithPeripheralScales.js +20 -1
  32. package/dist/lib/svg/SvgExporter.d.ts +1 -1
  33. package/dist/lib/svg/SvgExporter.js +4 -3
  34. package/dist/lib/svg/export.d.ts +3 -1
  35. package/dist/lib/traces/SmithData.d.ts +10 -4
  36. package/dist/lib/traces/SmithData.js +14 -4
  37. package/dist/lib/traces/TraceBuffer.d.ts +19 -0
  38. package/dist/lib/traces/TraceBuffer.js +176 -0
  39. package/dist/lib/traces/TraceLineDetail.d.ts +8 -0
  40. package/dist/lib/traces/TraceLineDetail.js +52 -0
  41. package/dist/lib/traces/TraceModel.d.ts +6 -8
  42. package/dist/lib/traces/TraceModel.js +24 -49
  43. package/dist/lib/traces/TraceRenderer.d.ts +13 -3
  44. package/dist/lib/traces/TraceRenderer.js +83 -23
  45. package/package.json +4 -2
package/README.md CHANGED
@@ -3,7 +3,14 @@
3
3
  **`smithkit`** is a framework-independent JavaScript and TypeScript library for
4
4
  interactive SVG Smith charts, RF calculations, and S11 measurement exploration.
5
5
 
6
- [Try the interactive demo](https://rosek86.github.io/smithkit/).
6
+ [Try the interactive demo](https://rosek86.github.io/smithkit/) or browse the
7
+ [integration examples](https://rosek86.github.io/smithkit/examples/index.html):
8
+ static charts, marker controls, a 100,000-sample trace, themes, report exports, and lifecycle cleanup.
9
+ Each example includes its TypeScript source.
10
+
11
+ Upgrading from 0.3.x? Version 0.4.0 introduces grouped constructor options and
12
+ `setOptions()`, static chart defaults, and explicit marker creation. See the
13
+ [0.4 migration guide](https://github.com/rosek86/smithkit/blob/v0.4.0/docs/migration-0.4.md).
7
14
 
8
15
  Upgrading from 0.2.x? Version 0.3.0 replaces `GridLayer.setMinorVisible()` with
9
16
  `setDetail('basic' | 'standard' | 'detailed')`. Replace `false` with `'standard'`
@@ -13,6 +20,10 @@ Upgrading from 0.1.x? Version 0.2.0 groups calculation, parsing, formatting, and
13
20
  comparison functions into classes. See the [migration guide](https://github.com/rosek86/smithkit/blob/v0.2.0/docs/migration-0.2.md)
14
21
  for the changed imports.
15
22
 
23
+ The [API review and compatibility policy](https://github.com/rosek86/smithkit/blob/main/docs/api-review.md)
24
+ define the public contract and the compatibility scope intended for 1.0. During
25
+ 0.x development, breaking changes are documented in minor releases.
26
+
16
27
  ## Features
17
28
 
18
29
  - Labeled resistance, reactance, conductance, and susceptance grids.
@@ -23,7 +34,7 @@ for the changed imports.
23
34
  - Physical impedance/admittance conversions and DOM-independent RF readings.
24
35
  - Twelve parameter scales mounted independently of chart zoom.
25
36
  - Four peripheral rulers for phase and electrical length.
26
- - One-port Touchstone 1.x import with RI, MA, and DB representations.
37
+ - Touchstone 1.x `.s1p` / `.s2p` reflection import (RI/MA/DB), packed output, and `.s1p` export.
27
38
  - Standalone SVG and configurable PNG export of the chart and radial scales.
28
39
  - ESM modules and TypeScript declarations.
29
40
 
@@ -54,7 +65,7 @@ Give the container an explicit size and run the code after it is mounted:
54
65
  ```ts
55
66
  import { Smith } from 'smithkit';
56
67
 
57
- const chart = new Smith(50); // Reference impedance in ohms; defaults to 50.
68
+ const chart = new Smith({ referenceImpedanceOhms: 50 }); // Reference impedance in ohms; defaults to 50.
58
69
  chart.draw('#smith');
59
70
  const traceId = chart.addTrace(
60
71
  [
@@ -71,33 +82,202 @@ coefficient Γ as `[real, imaginary]`. Samples are copied and validated; readonl
71
82
  must be finite and non-negative; both coordinates must be finite. A trace must
72
83
  contain at least one sample.
73
84
 
74
- Each new trace receives one marker on its first sample. Drag markers to select
75
- samples with a mouse or touch, scroll to zoom, and drag the chart to pan. Markers
85
+ Traces start without markers. Call `chart.addMarker(traceId)` to add one at the
86
+ first sample, or pass a sample index to choose its initial position. Drag markers to select
87
+ samples with a mouse or touch. Enable zoom to scroll to zoom and drag the chart to pan. Markers
76
88
  and their focus indicators stay above grid labels and sample points. `chart.resetView()` restores the initial view, including peripheral rulers.
77
89
 
78
90
  Construct `Smith` only in a browser, such as your framework's mount hook. Importing
79
91
  the package and using its calculation or parsing methods does not require a DOM.
80
92
 
81
- Zoom is enabled by default. For a fixed view, call `chart.setZoomEnabled(false)` before or after mounting.
82
- It disables wheel/double-click zoom and mouse/touch panning and pinch zoom while
83
- preserving the current view. Set it back to `true` to restore gestures. This is a
84
- view control: cursor readings and marker dragging stay available. `resetView()`
85
- also remains available when zoom is disabled. New wheel gestures do not consume
86
- page scrolling. Disabling during an active gesture freezes the view while that
87
- gesture finishes normally.
93
+ The default chart is a static presentation: standard impedance grid with labels,
94
+ no peripheral scales, and zoom and cursor tracking disabled. Markers are optional
95
+ and remain interactive when explicitly added.
96
+
97
+ Enable interactions through constructor options or the corresponding setters:
88
98
 
89
99
  ```ts
90
100
  import { Smith } from 'smithkit';
91
101
 
92
- const chart = new Smith(50);
93
- chart.setZoomEnabled(false);
102
+ const chart = new Smith({ interaction: { zoom: true, cursor: true } });
94
103
  chart.draw('#smith');
104
+
105
+ chart.setZoomEnabled(false); // Freeze the current view; page scrolling is unaffected.
106
+ chart.setCursorEnabled(false); // Hide cursor geometry and cancel pending readings.
95
107
  ```
96
108
 
109
+ Zoom controls wheel/double-click zoom and mouse/touch panning and pinch gestures;
110
+ it is independent of cursor tracking and marker interaction. `resetView()` remains
111
+ available with zoom disabled. Disabling during an active gesture freezes the view
112
+ while that gesture finishes normally.
113
+
114
+ Cursor tracking controls the cursor overlay and `Cursor` events. Disabling it
115
+ clears the current readout with one `Cursor` event carrying `undefined`, cancels
116
+ queued readings, and suppresses further cursor events. Enabling waits for a new
117
+ pointer movement. The last `cursorReading` remains available; marker controls and
118
+ events work independently.
119
+
97
120
  `draw(selector | HTMLElement)` moves the existing SVG when called again. A missing
98
121
  container throws. Call `chart.destroy()` on unmount; it removes the SVG, releases
99
122
  event handlers, and cancels queued notifications. Repeated destruction is safe.
100
123
 
124
+ ## Trace input formats
125
+
126
+ `addTrace()` and `updateTrace()` accept the same `TraceInput` union:
127
+
128
+ - `TraceSamples`: an array of objects with `frequencyHz` and `reflectionCoefficient`.
129
+ - `readonly TraceTuple[]`: `[frequencyHz, re, im]` tuples, including `as const` arrays.
130
+ - `Float64Array`: packed triples in the order `f, re, im, f, re, im, …`.
131
+
132
+ In every format, frequency is in **Hz** and `re`/`im` are the dimensionless
133
+ voltage reflection coefficient **Γ**, relative to the chart's reference impedance.
134
+ A single input must use one format throughout; mixed tuples and objects are rejected.
135
+ Typed-array views are supported; only values inside the supplied view are copied.
136
+ The packed length must be a positive multiple of three. All formats use the same
137
+ finite-value checks and retain input order, duplicate frequencies, and full sample precision.
138
+
139
+ ```ts
140
+ import { Smith, type TraceTuple } from 'smithkit';
141
+
142
+ const chart = new Smith();
143
+ chart.draw('#smith');
144
+ const samples: readonly TraceTuple[] = [
145
+ [1e9, 0.4, -0.4],
146
+ [1.5e9, 0, 0],
147
+ [2e9, 0.4, 0.4],
148
+ ];
149
+ const traceId = chart.addTrace(samples, { name: 'S11', mode: 'line' });
150
+
151
+ // A later update may use a different format. Each triple is one sample.
152
+ chart.updateTrace(traceId, new Float64Array([1e9, 0.3, -0.3, 1.5e9, 0, 0, 2e9, 0.3, 0.3]));
153
+ ```
154
+
155
+ For large datasets, fill the packed array directly to avoid creating intermediate
156
+ objects or tuples:
157
+
158
+ ```ts
159
+ import { Smith } from 'smithkit';
160
+
161
+ const count = 100_000;
162
+ const samples = new Float64Array(count * 3);
163
+ for (let i = 0; i < count; i++) {
164
+ samples[3 * i] = 1e9 + i * 1e4;
165
+ samples[3 * i + 1] = 0.7 * Math.cos((i / count) * 10);
166
+ samples[3 * i + 2] = 0.7 * Math.sin((i / count) * 10);
167
+ }
168
+ const chart = new Smith();
169
+ chart.draw('#smith');
170
+ const traceId = chart.addTrace(samples, { mode: 'points' });
171
+ chart.addMarker(traceId, count - 1);
172
+ ```
173
+
174
+ All inputs are copied into owned packed storage (24 bytes per sample for the numeric
175
+ payload). Modifying the input after adding or updating a trace does not change the
176
+ chart. Marker readings and events keep their existing object shape. `Touchstone.parse()` returns object samples by default, or packed triples with
177
+ `output: 'packed'`. `RfCalculations.renormalizeSamples()` preserves object or packed
178
+ representation.
179
+ Invalid replacements leave data, markers, and rendering unchanged.
180
+
181
+ For more than 5,000 samples, point rendering groups overlapping points into screen
182
+ cells and skips offscreen points; markers still select from the full data. Lines
183
+ retain every sample, so very large SVG paths can remain expensive to render and export.
184
+ Sorted ascending frequencies accelerate frequency selection; unsorted inputs remain
185
+ supported without reordering. There is no automatic interpolation or data reduction.
186
+
187
+ ## Constructor configuration
188
+
189
+ `new Smith(options?)` accepts a typed `SmithOptions` object. Every field is optional:
190
+
191
+ ```ts
192
+ import { Smith } from 'smithkit';
193
+
194
+ const chart = new Smith({
195
+ referenceImpedanceOhms: 75,
196
+ appearance: { theme: 'dark' },
197
+ interaction: { zoom: false },
198
+ peripheralScales: { visible: false },
199
+ grid: {
200
+ detail: 'basic',
201
+ labelsVisible: true,
202
+ style: { majorWidth: 1.5 },
203
+ layers: {
204
+ resistance: { style: { stroke: '#60a5fa' } },
205
+ reactance: { labelsVisible: false },
206
+ conductance: { visible: true, detail: 'standard' },
207
+ },
208
+ },
209
+ circles: {
210
+ q: { visible: true, values: [1, 2, 5] },
211
+ vswr: { visible: true, values: [2, 3], style: { stroke: '#fbbf24' } },
212
+ },
213
+ });
214
+ chart.draw('#smith');
215
+
216
+ // Update all four grids together, including hidden admittance layers.
217
+ chart.setGridDetail('standard');
218
+ // Individual overrides remain available.
219
+ chart.layers.reactance.setDetail('basic');
220
+ ```
221
+
222
+ Configuration is grouped by responsibility:
223
+
224
+ | Group | Purpose | Runtime API |
225
+ | ------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
226
+ | `referenceImpedanceOhms` | Physical reference impedance | `renormalize()` |
227
+ | `appearance` | Theme, fonts, colors, marker/cursor styling | `setAppearance()` |
228
+ | `interaction` | `zoom` and `cursor` tracking switches | `setZoomEnabled()`, `setCursorEnabled()` |
229
+ | `grid` | Shared detail, labels, styles; `layers` overrides for resistance/reactance/conductance/susceptance | `setGridDetail()`, `layers.resistance` and the other grid controls |
230
+ | `circles` | Independent Q and VSWR values, visibility, styles | `layers.q`, `layers.vswr` |
231
+ | `peripheralScales` | Visibility of all peripheral rulers together | `peripheralScales.setVisible()` |
232
+
233
+ Defaults are 50 Ω, the light theme, zoom and cursor disabled, peripheral scales hidden,
234
+ and standard grids with labels. Resistance/reactance are visible;
235
+ conductance/susceptance and Q/VSWR circles are hidden. Q defaults to
236
+ `[0.5, 1, 2, 5, 10]`; VSWR defaults to `[1.2, 1.5, 2, 3, 5, 10]`.
237
+
238
+ Shared `grid` settings apply to all four grids; `grid.layers` overrides individual
239
+ fields afterward. Styles merge by property. Explicit layer styles override theme
240
+ colors and remain in effect when `setAppearance()` changes the theme.
241
+ Options and circle arrays are copied; changing the original object does not update
242
+ the chart. Use the existing chart/layer methods for later changes. Unknown constructor groups
243
+ or structural option names throw `TypeError`; omitted fields use defaults.
244
+
245
+ Export configuration belongs to `toSvg()`/`toPng()` calls, independently of chart
246
+ construction. `scaleReadout` requires `scales`; otherwise export throws/rejects with `TypeError`.
247
+
248
+ The constructor configures presentation and interaction. Add measurement data with
249
+ `addTrace()` and optional markers with `addMarker()`; no markers are created implicitly.
250
+ Radial parameter scales remain independently mounted `SmithScales` instances.
251
+
252
+ ### Updating configuration
253
+
254
+ `chart.setOptions(options)` accepts the same `SmithOptions` type as the constructor:
255
+
256
+ ```ts
257
+ chart.setOptions({
258
+ interaction: { zoom: true },
259
+ grid: {
260
+ detail: 'basic',
261
+ layers: { reactance: { labelsVisible: false } },
262
+ },
263
+ circles: { vswr: { visible: true, values: [2, 3] } },
264
+ });
265
+ ```
266
+
267
+ This is a patch: omitted and `undefined` fields retain their current values;
268
+ `{}` groups do nothing. Shared grid settings apply to all four grids first, then
269
+ per-layer overrides in the same call. A later shared setting also replaces that
270
+ field on previously customized layers. Style fields merge; circle value lists
271
+ replace. `appearance`, when provided, replaces the preset/overrides exactly like
272
+ `setAppearance()` (it is not recursively merged with the previous appearance).
273
+
274
+ The complete patch is validated before application. Invalid settings or impossible
275
+ reference conversions leave the chart unchanged. `referenceImpedanceOhms` invokes
276
+ renormalization of existing traces and retains physical impedance and marker IDs.
277
+ Other configuration updates preserve measurements, markers, selection, and view.
278
+ Options are not retained; subsequent edits to the input object have no effect.
279
+ Individual setters remain available for focused changes.
280
+
101
281
  ### Keyboard and touch markers
102
282
 
103
283
  Use Tab/Shift+Tab to focus a marker. Arrow keys move through samples in input order;
@@ -124,7 +304,7 @@ there. They survive renaming, hiding, updating samples, and removing other items
124
304
  They are not persistent across chart instances or page reloads.
125
305
 
126
306
  ```ts
127
- const markerA = chart.getTraces().find((trace) => trace.id === traceId)!.markers[0].id;
307
+ const markerA = chart.addMarker(traceId)!; // First sample.
128
308
  const markerB = chart.addMarker(traceId, 2)!; // Zero-based sample index.
129
309
 
130
310
  chart.setTraceOptions(traceId, { name: 'Tuned antenna', visible: true });
@@ -141,7 +321,7 @@ chart.removeMarker(markerB);
141
321
 
142
322
  | Method | Result / behavior |
143
323
  | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
144
- | `addTrace(samples, options?)` | Returns a trace ID. Options: nonblank `name`, solid CSS `color`, and `visible`. |
324
+ | `addTrace(samples, options?)` | Returns a trace ID, without creating markers. Options: nonblank `name`, solid CSS `color`, and `visible`. |
145
325
  | `getTraces()` | Detached `TraceInfo` snapshots: ID, name, color, visibility, sample count, and marker metadata. |
146
326
  | `setTraceOptions(id, options)` | Updates the supplied options only. Invalid options throw before any change. |
147
327
  | `updateTrace(id, samples, options?)` | Replaces samples and preserves identity, appearance, and markers. Empty/invalid input throws without changing the trace. |
@@ -191,21 +371,41 @@ after destruction throw; trace/marker lookups return empty or missing results.
191
371
 
192
372
  ## Trace appearance
193
373
 
194
- Pass `mode: 'points' | 'line' | 'both'`, `lineWidth`, and `pointRadius` to
374
+ Pass `mode: 'points' | 'line' | 'both'`, `lineWidth`, `pointRadius`, and `lineTolerancePx` to
195
375
  `addTrace` or `setTraceOptions`. Defaults are points, 2 px line width, and 2 px point radius. Both sizes are
196
376
  CSS pixels and remain constant under chart zoom and container resize. Widths and radii must be positive and finite.
197
377
  `getTraces()` includes the current style.
198
378
 
199
- Lines connect all samples in input order, without smoothing or frequency sorting.
379
+ By default, lines connect all samples in input order, without smoothing or frequency sorting.
200
380
  A line-only trace creates one SVG path and no sample circles, suitable for dense
201
381
  sweeps. Points/both render every sample for traces up to 5,000 samples. Larger traces
202
382
  render one representative per point-radius-sized display cell and skip points
203
383
  outside the viewport; representatives are recalculated during zoom/pan.
204
384
  This only simplifies the image: all original samples remain available to markers,
205
- snapping, readouts, and comparisons. Lines still connect every input sample.
385
+ snapping, readouts, and comparisons.
206
386
  Styles do not change marker identity or layer ordering. Hidden traces retain their style after sample replacement.
207
387
  The demo exposes the same controls per trace.
208
388
 
389
+ For dense lines, opt into `lineTolerancePx` (finite, non-negative; default `0` disables
390
+ simplification):
391
+
392
+ ```ts
393
+ chart.setTraceOptions(traceId, { mode: 'line', lineTolerancePx: 0.5 });
394
+ ```
395
+
396
+ The tolerance bounds additional geometric deviation in **CSS pixels**, accounting for
397
+ chart zoom and container size. It does not include SVG coordinate rounding or browser
398
+ antialiasing. Cached, nested geometry levels retain endpoints and input order; zooming
399
+ in selects finer geometry, while panning reuses the current level. The cache is rebuilt
400
+ when samples change, including reference-impedance renormalization. Markers, frequency
401
+ selection, RF calculations, and sample counts always use the full data. The option
402
+ applies to the line in `line` and `both` modes; point rendering is unchanged.
403
+
404
+ Preparing levels takes extra CPU and memory, and noisy traces may simplify very little.
405
+ This is an opt-in display optimization, not data compression. See the
406
+ [large-trace example](https://rosek86.github.io/smithkit/examples/large-trace/) to compare
407
+ full geometry with pixel tolerances and [local measurements](docs/performance.md).
408
+
209
409
  ## Events and readings
210
410
 
211
411
  `onEvent(listener)` supports multiple independent subscriptions and returns an
@@ -240,8 +440,8 @@ queued `Marker` event and do not emit drag lifecycle events.
240
440
  Cursor movement is suspended during marker dragging. Drag start/end events fire
241
441
  synchronously; marker position notifications are queued and report the latest
242
442
  snapshot at delivery. Before delivery, repeated updates to the same marker coalesce
243
- into one notification; different markers have independent notifications. Creation
244
- queues an initial marker notification too. Removed markers cannot deliver queued
443
+ into one notification; different markers have independent notifications. `addMarker()`
444
+ queues an initial marker notification too; `addTrace()` does not. Removed markers cannot deliver queued
245
445
  position events.
246
446
  Trace metadata changes do not emit marker events; refresh metadata after mutations.
247
447
  Destruction suppresses application callbacks.
@@ -368,7 +568,7 @@ const appearance: SmithAppearance = {
368
568
  traceColors: ['#38bdf8', '#fb923c', '#a78bfa'],
369
569
  },
370
570
  };
371
- const chart = new Smith(50, appearance);
571
+ const chart = new Smith({ referenceImpedanceOhms: 50, appearance });
372
572
  const scales = new SmithScales(appearance);
373
573
  chart.draw('#smith');
374
574
  scales.draw('#scales');
@@ -437,12 +637,13 @@ layout raises it when needed to keep the font size at least 9 CSS pixels in the
437
637
  default view. Partial updates retain other settings. Invalid numeric styles throw
438
638
  before any property is changed.
439
639
 
440
- Choose `setDetail('basic' | 'standard' | 'detailed')` independently for each grid layer:
640
+ Use `chart.setGridDetail('basic' | 'standard' | 'detailed')` for all four grid layers,
641
+ or `layer.setDetail(...)` to override one layer:
441
642
 
442
643
  - **Basic:** complete circles and arcs for normalized values 0.2, 0.5, 1, 2, and 5,
443
644
  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.
645
+ - **Standard** (default): the full major grid and its labels, without minor lines.
646
+ - **Detailed:** the full major and minor grid.
446
647
 
447
648
  Changing detail preserves layer visibility, styles, and the current view. Label
448
649
  collision handling still applies at every level. Peripheral scales and Q/VSWR
@@ -455,7 +656,10 @@ whole-layer visibility changes and is included in SVG exports. Calling
455
656
  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.
456
657
 
457
658
  `layers.q` and `layers.vswr` support `setVisible`,
458
- `setStyle({ stroke?, strokeWidth? })`, `addValue`, and `removeValue`.
659
+ `setStyle({ stroke?, strokeWidth? })`, `setValues`, `addValue`, and `removeValue`.
660
+ `setValues([1, 2, 5])` replaces the entire list; `setValues([])` clears it.
661
+ Input arrays are copied, duplicate values are ignored, and invalid input leaves
662
+ the previous values unchanged.
459
663
  Circle stroke widths are positive finite numbers in CSS pixels.
460
664
  Q values must be positive and finite; VSWR values must be finite and at least 1.
461
665
  Duplicate additions and removal of absent values have no effect.
@@ -515,7 +719,32 @@ assumes a matched line or attenuator terminated in an open or short: the reflect
515
719
  wave traverses it twice. It does not measure insertion loss of an arbitrary load
516
720
  from S11 alone. At \|Γ\| = 0.1 it reads 10 dB, while return loss reads 20 dB.
517
721
 
518
- `chart.peripheralScales` supports `setVisible(boolean)` and `update(Complex | null)`.
722
+ Peripheral rulers are hidden by default. Enable them with
723
+ `{ peripheralScales: { visible: true } }` or `chart.peripheralScales.setVisible(true)`.
724
+ Axis captions and numeric tick labels can be controlled independently, through the
725
+ constructor, `setOptions()`, or the peripheral scale controls:
726
+
727
+ ```ts
728
+ import { Smith } from 'smithkit';
729
+
730
+ const chart = new Smith({
731
+ peripheralScales: { visible: true, captionsVisible: false, tickLabelsVisible: true },
732
+ });
733
+ chart.draw('#smith');
734
+
735
+ chart.setOptions({ peripheralScales: { captionsVisible: true } });
736
+ chart.peripheralScales.setTickLabelsVisible(false); // Captions and rulers remain visible.
737
+ chart.peripheralScales.setCaptionsVisible(false); // Rulers and indicators remain visible.
738
+ ```
739
+
740
+ Both text options default to `true`; the entire group remains hidden by default.
741
+ When captions are hidden, numeric labels fill the sectors normally reserved for them.
742
+ Hiding/re-showing the rulers preserves both preferences. Text controls do not affect
743
+ ruler lines, ticks, indicators, grid labels, or the independent radial parameter scales.
744
+ SVG/PNG exports preserve the current text visibility. The demo exposes **Axis captions**
745
+ and **Tick values** checkboxes in **Chart settings**.
746
+
747
+ `chart.peripheralScales.update(Complex | null)` supplies or clears the indicators.
519
748
  Its four rulers zoom with the chart: reflection phase, voltage transmission phase,
520
749
  wavelengths toward generator, and wavelengths toward load. Wavelength rulers start
521
750
  at the negative real axis, with 0.5 λ per revolution (generator clockwise, load
@@ -554,28 +783,81 @@ Import renormalization is enabled by a labeled checkbox. Disable it to reject
554
783
  files whose reference differs from the chart. Complex reference impedances and
555
784
  multiport renormalization are outside the supported scope.
556
785
 
557
- ## Touchstone import
786
+ ## Touchstone import and export
787
+
788
+ `Touchstone.parse(text, options?)` returns `{ samples, referenceImpedanceOhms }`.
789
+ It reads Touchstone 1.x S-parameters, converts RI/MA/DB into Cartesian Γ and
790
+ frequencies into Hz, and throws an `Error` with a line number for invalid content.
791
+ Invalid parser options throw `TypeError`.
558
792
 
559
- `Touchstone.parse(text)` returns `{ samples, referenceImpedanceOhms }`. It accepts
560
- one-port Touchstone 1.x, converts RI/MA/DB into Cartesian Γ and all frequencies
561
- into Hz, and throws for unsupported or invalid input.
793
+ | Option | Default | Meaning |
794
+ | ----------- | ----------- | -------------------------------------------------------------------------------- |
795
+ | `ports` | `1` | Source port count: `1` for `.s1p`, `2` for `.s2p`. It is not inferred from text. |
796
+ | `parameter` | `'S11'` | Reflection parameter: `'S11'` or, with two ports, `'S22'`. |
797
+ | `output` | `'objects'` | Object samples or `'packed'` `Float64Array` containing f/re/im triples. |
562
798
 
563
799
  ```ts
564
800
  import { Smith, Touchstone } from 'smithkit';
565
801
 
566
802
  async function showMeasurement(file: File): Promise<Smith> {
567
- const { samples, referenceImpedanceOhms } = Touchstone.parse(await file.text());
568
- const chart = new Smith(referenceImpedanceOhms);
803
+ const ports = /\.s2p$/i.test(file.name) ? 2 : 1;
804
+ const { samples, referenceImpedanceOhms } = Touchstone.parse(await file.text(), {
805
+ ports,
806
+ parameter: ports === 2 ? 'S22' : 'S11',
807
+ output: 'packed',
808
+ });
809
+ const chart = new Smith({ referenceImpedanceOhms });
569
810
  chart.draw('#smith');
570
811
  chart.addTrace(samples, { name: file.name });
571
812
  return chart;
572
813
  }
573
814
  ```
574
815
 
575
- Omitted options use Touchstone defaults: GHz, S, MA, 50 Ω. For an existing chart,
576
- use `RfCalculations.renormalizeSamples` when the file reference differs from
577
- `chart.referenceImpedanceOhms`. The parser itself does not renormalize data.
578
- Multiport data and Touchstone 2.x are not supported.
816
+ Packed parsing writes directly into growing typed storage, without creating an
817
+ object or coordinate array per sample. It scans lines incrementally within the
818
+ supplied string; this is not a streaming file API. The returned buffer has exact
819
+ length and is caller-owned. The default object result remains unchanged.
820
+
821
+ Omitted header options use GHz, S, MA, 50 Ω. The parser accepts comments, BOM,
822
+ LF/CRLF/CR line endings, decimal scientific notation, and continued records. Each
823
+ frequency record starts on a new line. Two-port records use S11, S21, S12, S22
824
+ order; all four parameters are validated even though only one reflection trace is
825
+ returned. Two-port frequencies must increase strictly. Three or more ports,
826
+ per-port/complex reference impedances, noise blocks and Touchstone 2.x keywords
827
+ are rejected. See the [IBIS Touchstone specification](https://www.ibis.org/touchstone_ver2.1/touchstone_ver2_1.pdf)
828
+ for the legacy 1.x data layout.
829
+
830
+ `Touchstone.stringify(samples, { referenceImpedanceOhms })` returns `.s1p` text
831
+ with a `# Hz S RI R …` header and a final newline. It accepts object samples,
832
+ tuples, or packed triples, writes numeric values without a precision limit, and
833
+ does not modify or renormalize the input. The reference defaults to 50 Ω; supply
834
+ the actual reference of your data. Empty/invalid samples, nonpositive/nonfinite
835
+ reference impedance, and duplicate or descending frequencies throw `RangeError`.
836
+ Samples are never silently sorted or discarded.
837
+
838
+ ```ts
839
+ import { RfCalculations, Touchstone } from 'smithkit';
840
+
841
+ const sourceText = '# GHz S RI R 50\n1 0 0\n2 0.5 0';
842
+ const parsed = Touchstone.parse(sourceText, { output: 'packed' });
843
+ const samples = RfCalculations.renormalizeSamples(
844
+ parsed.samples,
845
+ parsed.referenceImpedanceOhms,
846
+ 75,
847
+ );
848
+ const text = Touchstone.stringify(samples, { referenceImpedanceOhms: 75 });
849
+ // Save text as measurement.s1p in your application.
850
+ ```
851
+
852
+ For an extracted S11/S22 trace, this is **one-port renormalization**: the other
853
+ port remains terminated in the original file reference. It is not renormalization
854
+ of the complete two-port network. Exporting S22 to `.s1p` represents that selected
855
+ port as the single port of the new file; transmission data is not exported.
856
+
857
+ The [Touchstone example](https://rosek86.github.io/smithkit/examples/touchstone/index.html)
858
+ shows parameter selection, packed import, renormalization and download. In the demo,
859
+ choose **Two-port reflection** before importing `.s2p`; the trace name includes the
860
+ selected parameter. Both `.s1p` and `.s2p` use packed import.
579
861
 
580
862
  ## PNG and report export
581
863
 
@@ -602,6 +884,11 @@ const scalePng = await scales.toPng({ width: 1200, background: 'transparent' });
602
884
  - `background`: omit to keep component backgrounds, or pass a concrete CSS color
603
885
  (for example `'#fff'`) or `'transparent'` to replace them. Chart colors stay as
604
886
  configured; choose a suitable chart theme for your report background.
887
+ - `lineDetail` (chart only): `'full'` (default) includes every sample in each trace line;
888
+ `'view'` preserves the currently displayed line geometry, including simplification.
889
+ Both work for SVG and PNG without changing the live chart. Increasing export dimensions
890
+ scales the current-view error too; use `'full'` for reports requiring all samples.
891
+ This option does not change point culling, the viewport, markers, or scales.
605
892
  - `legend` (chart only): append the names and colors of visible traces. Long names
606
893
  wrap; hidden traces are omitted. Default: `false`.
607
894
  - `markerLegend` (chart only): independently append marker descriptions below the chart/scales.
@@ -0,0 +1,16 @@
1
+ import type { SmithOptions } from './options.js';
2
+ import type { CircleStyle, GridDetail, GridStyle } from './layers.js';
3
+ /** Shared validation for initial configuration, patches, and individual controls. */
4
+ export declare class OptionsValidation {
5
+ private constructor();
6
+ static object(value: unknown, name: string, keys?: readonly string[]): void;
7
+ static boolean(value: boolean, name: string): void;
8
+ static detail(value: GridDetail): void;
9
+ private static lengths;
10
+ private static strings;
11
+ static gridStyle(style: Partial<GridStyle>): void;
12
+ static circleStyle(style: Partial<CircleStyle>): void;
13
+ static circleValue(value: number, minimum: number): void;
14
+ static circleValues(values: readonly number[], minimum: number): void;
15
+ static chart(options: SmithOptions): void;
16
+ }