smithkit 0.1.0 → 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 +124 -59
  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 +78 -35
  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 +6 -6
  11. package/dist/lib/{draw → grid}/ConstCircles.js +12 -5
  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 +7 -2
  51. package/dist/lib/scales/SmithPeripheralScales.js +65 -28
  52. package/dist/lib/scales/SmithScales.d.ts +3 -1
  53. package/dist/lib/scales/SmithScales.js +20 -6
  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
@@ -0,0 +1,152 @@
1
+ import { line } from 'd3';
2
+ import { SmithGroup } from '../svg/SmithGroup.js';
3
+ /** Draws a trace; sample validation and marker selection belong to TraceModel. */
4
+ export class TraceRenderer {
5
+ data;
6
+ color;
7
+ transform;
8
+ scaler;
9
+ style;
10
+ viewportScale = 1;
11
+ visible = true;
12
+ group;
13
+ constructor(data, color, transform, container, scaler, style = {}) {
14
+ this.data = data;
15
+ this.color = color;
16
+ this.transform = transform;
17
+ this.scaler = scaler;
18
+ this.style = {
19
+ mode: style.mode ?? 'points',
20
+ lineWidth: style.lineWidth ?? 2,
21
+ pointRadius: style.pointRadius ?? 2,
22
+ };
23
+ this.group = this.drawTrace(data).attr('pointer-events', 'none');
24
+ container.append(this.group);
25
+ }
26
+ drawTrace(data) {
27
+ const group = new SmithGroup({
28
+ stroke: 'none',
29
+ strokeWidth: 'none',
30
+ fill: this.color,
31
+ });
32
+ group.attr('data-role', 'samples');
33
+ group.attr('data-mode', this.style.mode);
34
+ if (this.style.mode !== 'points') {
35
+ const path = line()
36
+ .x((sample) => this.scaler.x(sample.reflectionCoefficient[0]))
37
+ .y((sample) => this.scaler.y(sample.reflectionCoefficient[1]));
38
+ group.Element.append('path')
39
+ .attr('class', 'trace-line')
40
+ .attr('d', path(data))
41
+ .attr('fill', 'none')
42
+ .attr('stroke', this.color)
43
+ .attr('stroke-width', this.style.lineWidth)
44
+ .attr('vector-effect', 'non-scaling-stroke')
45
+ .attr('stroke-linejoin', 'round')
46
+ .attr('stroke-linecap', 'round');
47
+ }
48
+ if (this.style.mode !== 'line') {
49
+ this.renderPoints(group, data);
50
+ }
51
+ return group;
52
+ }
53
+ renderPoints(group, data) {
54
+ let visible = data;
55
+ if (data.length > 5000) {
56
+ const cells = new Set();
57
+ const cellSize = Math.max(0.5, this.style.pointRadius) / this.viewportScale;
58
+ const extent = this.scaler.x(1);
59
+ const margin = this.style.pointRadius / this.viewportScale;
60
+ visible = data.filter((sample) => {
61
+ const x = this.scaler.x(sample.reflectionCoefficient[0]) * this.transform.k + this.transform.x;
62
+ const y = this.scaler.y(sample.reflectionCoefficient[1]) * this.transform.k + this.transform.y;
63
+ if (x < -margin || y < -margin || x > extent + margin || y > extent + margin) {
64
+ return false;
65
+ }
66
+ const cell = `${Math.floor(x / cellSize)},${Math.floor(y / cellSize)}`;
67
+ if (cells.has(cell)) {
68
+ return false;
69
+ }
70
+ cells.add(cell);
71
+ return true;
72
+ });
73
+ }
74
+ group.Element.selectAll('circle')
75
+ .data(visible)
76
+ .join('circle')
77
+ .attr('cx', (sample) => this.scaler.x(sample.reflectionCoefficient[0]))
78
+ .attr('cy', (sample) => this.scaler.y(sample.reflectionCoefficient[1]))
79
+ .attr('r', this.style.pointRadius / (this.transform.k * this.viewportScale));
80
+ }
81
+ setViewportScale(scale) {
82
+ if (scale === this.viewportScale || !Number.isFinite(scale) || scale <= 0) {
83
+ return;
84
+ }
85
+ this.viewportScale = scale;
86
+ this.zoomDataPoints();
87
+ }
88
+ zoom(transform) {
89
+ this.transform = transform;
90
+ this.zoomDataPoints();
91
+ }
92
+ zoomDataPoints() {
93
+ const k = this.transform.k;
94
+ if (this.style.mode === 'line') {
95
+ return;
96
+ }
97
+ if (this.data.length > 5000) {
98
+ this.renderPoints(this.group, this.data);
99
+ }
100
+ else {
101
+ this.group.Element.selectAll('circle').attr('r', this.style.pointRadius / (k * this.viewportScale));
102
+ }
103
+ }
104
+ setColor(color) {
105
+ this.color = color;
106
+ this.group.attr('fill', color);
107
+ this.group.Element.select('.trace-line').attr('stroke', color);
108
+ }
109
+ get Style() {
110
+ return { ...this.style };
111
+ }
112
+ setStyle(options) {
113
+ const next = {
114
+ mode: options.mode ?? this.style.mode,
115
+ lineWidth: options.lineWidth ?? this.style.lineWidth,
116
+ pointRadius: options.pointRadius ?? this.style.pointRadius,
117
+ };
118
+ if (next.mode === this.style.mode &&
119
+ next.lineWidth === this.style.lineWidth &&
120
+ next.pointRadius === this.style.pointRadius) {
121
+ return;
122
+ }
123
+ const modeChanged = next.mode !== this.style.mode;
124
+ this.style = next;
125
+ if (modeChanged) {
126
+ this.redraw();
127
+ }
128
+ else {
129
+ this.group.Element.select('.trace-line').attr('stroke-width', next.lineWidth);
130
+ this.zoomDataPoints();
131
+ }
132
+ }
133
+ redraw() {
134
+ const group = this.drawTrace(this.data).attr('pointer-events', 'none');
135
+ // Replace in place to retain trace ordering beneath the separate marker layer.
136
+ this.group.Node.replaceWith(group.Node);
137
+ this.group = group;
138
+ this.group.Element.style('display', () => (this.visible ? null : 'none'));
139
+ }
140
+ setVisible(visible) {
141
+ this.visible = visible;
142
+ this.group.Element.style('display', () => (visible ? null : 'none'));
143
+ }
144
+ update(data) {
145
+ this.data = data;
146
+ this.redraw();
147
+ }
148
+ destroy() {
149
+ this.data = [];
150
+ this.group.Element.remove();
151
+ }
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smithkit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-independent, interactive Smith chart library for JavaScript and TypeScript",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -31,10 +31,13 @@
31
31
  "@eslint/js": "^10.0.1",
32
32
  "@playwright/test": "^1.63.0",
33
33
  "@types/node": "^24.0.0",
34
+ "@types/pngjs": "^6.0.5",
34
35
  "@typescript/native": "npm:typescript@^7.0.2",
35
36
  "eslint": "^10.12.0",
36
37
  "eslint-config-prettier": "^10.1.8",
37
38
  "globals": "^17.13.0",
39
+ "pixelmatch": "^8.0.0",
40
+ "pngjs": "^7.0.0",
38
41
  "prettier": "3.9.9",
39
42
  "typescript": "npm:@typescript/typescript6@^6.0.2",
40
43
  "typescript-eslint": "^8.71.0",
@@ -1,10 +0,0 @@
1
- import { createGridDefinitions } from './grid/definitions.js';
2
- import { resistanceLabels, reactanceLabels } from './grid/labels.js';
3
- export { SmithArcEntry } from './grid/types.js';
4
- export type { SmithArcDef, SmithTickDef, SmithTicksShapes, SmithTicksData } from './grid/types.js';
5
- export declare class SmithArcsDefs {
6
- private constructor();
7
- static getData: typeof createGridDefinitions;
8
- static resistanceLabels: typeof resistanceLabels;
9
- static reactanceLabels: typeof reactanceLabels;
10
- }
@@ -1,10 +0,0 @@
1
- import { createGridDefinitions } from './grid/definitions.js';
2
- import { resistanceLabels, reactanceLabels } from './grid/labels.js';
3
- // Compatibility facade; definitions and label rules are independent of SVG rendering.
4
- export { SmithArcEntry } from './grid/types.js';
5
- export class SmithArcsDefs {
6
- constructor() { }
7
- static getData = createGridDefinitions;
8
- static resistanceLabels = resistanceLabels;
9
- static reactanceLabels = reactanceLabels;
10
- }
@@ -1,14 +0,0 @@
1
- /** An ideal series component equivalent to a reactance at one frequency. */
2
- export type ReactiveComponent = {
3
- readonly kind: 'inductor';
4
- readonly inductanceHenries: number;
5
- } | {
6
- readonly kind: 'capacitor';
7
- readonly capacitanceFarads: number;
8
- };
9
- /**
10
- * Convert reactance in ohms at a frequency in Hz to an ideal series L or C.
11
- * Zero frequency, zero reactance, or an unrepresentable component returns undefined.
12
- * Non-finite reactance and negative/non-finite frequency throw RangeError.
13
- */
14
- export declare function reactanceToComponent(reactanceOhms: number, frequencyHz: number): ReactiveComponent | undefined;
@@ -1,26 +0,0 @@
1
- /**
2
- * Convert reactance in ohms at a frequency in Hz to an ideal series L or C.
3
- * Zero frequency, zero reactance, or an unrepresentable component returns undefined.
4
- * Non-finite reactance and negative/non-finite frequency throw RangeError.
5
- */
6
- export function reactanceToComponent(reactanceOhms, frequencyHz) {
7
- if (!Number.isFinite(reactanceOhms)) {
8
- throw new RangeError('Reactance must be finite.');
9
- }
10
- if (!Number.isFinite(frequencyHz) || frequencyHz < 0) {
11
- throw new RangeError('Frequency must be finite and non-negative.');
12
- }
13
- if (frequencyHz === 0 || reactanceOhms === 0) {
14
- return;
15
- }
16
- // Compute in log space so intermediate products/quotients cannot overflow.
17
- const logReactance = Math.log(Math.abs(reactanceOhms));
18
- const logAngularFrequency = Math.log(2 * Math.PI) + Math.log(frequencyHz);
19
- const value = Math.exp((reactanceOhms > 0 ? logReactance : -logReactance) - logAngularFrequency);
20
- if (!Number.isFinite(value) || value === 0) {
21
- return;
22
- }
23
- return reactanceOhms > 0
24
- ? { kind: 'inductor', inductanceHenries: value }
25
- : { kind: 'capacitor', capacitanceFarads: value };
26
- }
@@ -1,366 +0,0 @@
1
- import { SmithGroup } from './SmithGroup.js';
2
- import { line } from 'd3';
3
- import { SmithMarker } from './SmithMarker.js';
4
- export class SmithData {
5
- data;
6
- color;
7
- transform;
8
- fgContainer;
9
- scaler;
10
- markerDragHandler;
11
- markerContainer;
12
- style;
13
- viewportScale = 1;
14
- destroyed = false;
15
- visible = true;
16
- pendingEvents = new Map();
17
- group;
18
- markersCount = 0;
19
- markers = [];
20
- handler = null;
21
- constructor(data, color, transform, fgContainer, scaler, markerDragHandler, markerContainer = fgContainer, style = {}) {
22
- this.data = data;
23
- this.color = color;
24
- this.transform = transform;
25
- this.fgContainer = fgContainer;
26
- this.scaler = scaler;
27
- this.markerDragHandler = markerDragHandler;
28
- this.markerContainer = markerContainer;
29
- this.style = {
30
- mode: style.mode ?? 'points',
31
- lineWidth: style.lineWidth ?? 2,
32
- pointRadius: style.pointRadius ?? 2,
33
- };
34
- this.data = this.copySamples(data);
35
- this.group = this.drawTrace(this.data);
36
- this.group.attr('pointer-events', 'none');
37
- this.fgContainer.append(this.group);
38
- this.zoomDataPoints();
39
- }
40
- drawTrace(data) {
41
- const group = new SmithGroup({
42
- stroke: 'none',
43
- strokeWidth: 'none',
44
- fill: this.color,
45
- });
46
- group.attr('data-role', 'samples');
47
- group.attr('data-mode', this.style.mode);
48
- if (this.style.mode !== 'points') {
49
- const path = line()
50
- .x((sample) => this.scaler.x(sample.reflectionCoefficient[0]))
51
- .y((sample) => this.scaler.y(sample.reflectionCoefficient[1]));
52
- group.Element.append('path')
53
- .attr('class', 'trace-line')
54
- .attr('d', path(data))
55
- .attr('fill', 'none')
56
- .attr('stroke', this.color)
57
- .attr('stroke-width', this.style.lineWidth)
58
- .attr('vector-effect', 'non-scaling-stroke')
59
- .attr('stroke-linejoin', 'round')
60
- .attr('stroke-linecap', 'round');
61
- }
62
- if (this.style.mode !== 'line') {
63
- this.renderPoints(group, data);
64
- }
65
- return group;
66
- }
67
- renderPoints(group, data) {
68
- let visible = data;
69
- if (data.length > 5000) {
70
- const cells = new Set();
71
- const cellSize = Math.max(0.5, this.style.pointRadius) / this.viewportScale;
72
- const extent = this.scaler.x(1);
73
- const margin = this.style.pointRadius / this.viewportScale;
74
- visible = data.filter((sample) => {
75
- const x = this.scaler.x(sample.reflectionCoefficient[0]) * this.transform.k + this.transform.x;
76
- const y = this.scaler.y(sample.reflectionCoefficient[1]) * this.transform.k + this.transform.y;
77
- if (x < -margin || y < -margin || x > extent + margin || y > extent + margin) {
78
- return false;
79
- }
80
- const cell = `${Math.floor(x / cellSize)},${Math.floor(y / cellSize)}`;
81
- if (cells.has(cell)) {
82
- return false;
83
- }
84
- cells.add(cell);
85
- return true;
86
- });
87
- }
88
- group.Element.selectAll('circle')
89
- .data(visible)
90
- .join('circle')
91
- .attr('cx', (sample) => this.scaler.x(sample.reflectionCoefficient[0]))
92
- .attr('cy', (sample) => this.scaler.y(sample.reflectionCoefficient[1]))
93
- .attr('r', this.style.pointRadius / (this.transform.k * this.viewportScale));
94
- }
95
- setViewportScale(scale) {
96
- if (scale === this.viewportScale || !Number.isFinite(scale) || scale <= 0) {
97
- return;
98
- }
99
- this.viewportScale = scale;
100
- this.zoomDataPoints();
101
- }
102
- zoom(transform) {
103
- if (this.destroyed) {
104
- return;
105
- }
106
- this.transform = transform;
107
- this.zoomDataPoints();
108
- this.zoomAllMarkers();
109
- }
110
- zoomDataPoints() {
111
- const k = this.transform.k;
112
- if (this.style.mode === 'line') {
113
- return;
114
- }
115
- if (this.data.length > 5000) {
116
- this.renderPoints(this.group, this.data);
117
- }
118
- else {
119
- this.group.Element.selectAll('circle').attr('r', this.style.pointRadius / (k * this.viewportScale));
120
- }
121
- }
122
- zoomAllMarkers() {
123
- const k = this.transform.k;
124
- for (const marker of this.markers) {
125
- const entry = marker.selectedPoint;
126
- if (entry) {
127
- marker.marker.zoom(k);
128
- }
129
- }
130
- }
131
- addMarker(sampleIndex = 0) {
132
- if (this.destroyed) {
133
- throw new Error('This dataset has been removed.');
134
- }
135
- this.validateSampleIndex(sampleIndex);
136
- const markerIndex = this.markersCount++;
137
- const marker = new SmithMarker(markerIndex + 1, this.color, (dragging) => this.markerDragHandler?.(marker, dragging));
138
- const markerDesc = {
139
- marker,
140
- selectedPoint: this.data[sampleIndex],
141
- number: markerIndex + 1,
142
- };
143
- this.markers.push(markerDesc);
144
- marker.Element.attr('data-role', 'marker');
145
- this.markerContainer.append(marker);
146
- marker.setDragHandler((mp) => {
147
- const dp = this.findClosestPointTo(this.scaler.pointInvert(mp));
148
- if (markerDesc.selectedPoint === dp) {
149
- return;
150
- }
151
- markerDesc.selectedPoint = dp;
152
- marker.move(this.scaler.point(dp.reflectionCoefficient));
153
- marker.zoom(this.transform.k);
154
- this.notifyMarker(markerDesc);
155
- });
156
- marker.zoom(this.transform.k);
157
- marker.show();
158
- marker.move(this.scaler.point(markerDesc.selectedPoint.reflectionCoefficient));
159
- marker.Element.style('display', () => (this.visible ? null : 'none'));
160
- this.notifyMarker(markerDesc);
161
- return this.markers.length - 1;
162
- }
163
- removeMarker(index) {
164
- const entry = this.markers[index];
165
- if (!entry) {
166
- return false;
167
- }
168
- clearTimeout(this.pendingEvents.get(entry));
169
- this.pendingEvents.delete(entry);
170
- entry.marker.destroy();
171
- this.markers.splice(index, 1);
172
- return true;
173
- }
174
- setMarkerSample(index, sampleIndex) {
175
- const entry = this.markers[index];
176
- if (!entry) {
177
- return false;
178
- }
179
- this.validateSampleIndex(sampleIndex);
180
- entry.selectedPoint = this.data[sampleIndex];
181
- entry.marker.move(this.scaler.point(entry.selectedPoint.reflectionCoefficient));
182
- this.notifyMarker(entry);
183
- return true;
184
- }
185
- setMarkerFrequency(index, frequencyHz) {
186
- if (!this.markers[index]) {
187
- return false;
188
- }
189
- if (!Number.isFinite(frequencyHz) || frequencyHz < 0) {
190
- throw new RangeError('Marker frequency must be finite and non-negative.');
191
- }
192
- return this.setMarkerSample(index, this.nearestFrequencyIndex(frequencyHz));
193
- }
194
- nearestFrequencyIndex(frequencyHz) {
195
- let closest = 0;
196
- let distance = Math.abs(this.data[0].frequencyHz - frequencyHz);
197
- for (let i = 1; i < this.data.length; i++) {
198
- const nextDistance = Math.abs(this.data[i].frequencyHz - frequencyHz);
199
- if (nextDistance < distance) {
200
- closest = i;
201
- distance = nextDistance;
202
- }
203
- }
204
- return closest;
205
- }
206
- validateSampleIndex(index) {
207
- if (!Number.isInteger(index) || index < 0 || index >= this.data.length) {
208
- throw new RangeError('Sample index is outside this trace.');
209
- }
210
- }
211
- get Samples() {
212
- return this.data;
213
- }
214
- get SampleCount() {
215
- return this.data.length;
216
- }
217
- get Color() {
218
- return this.color;
219
- }
220
- get Visible() {
221
- return this.visible;
222
- }
223
- markerSampleIndex(index) {
224
- const entry = this.markers[index];
225
- return entry ? this.data.indexOf(entry.selectedPoint) : -1;
226
- }
227
- setColor(color) {
228
- this.color = color;
229
- this.group.attr('fill', color);
230
- this.group.Element.select('.trace-line').attr('stroke', color);
231
- this.markers.forEach(({ marker }) => marker.setColor(color));
232
- }
233
- get Style() {
234
- return { ...this.style };
235
- }
236
- setStyle(options) {
237
- const next = {
238
- mode: options.mode ?? this.style.mode,
239
- lineWidth: options.lineWidth ?? this.style.lineWidth,
240
- pointRadius: options.pointRadius ?? this.style.pointRadius,
241
- };
242
- if (next.mode === this.style.mode &&
243
- next.lineWidth === this.style.lineWidth &&
244
- next.pointRadius === this.style.pointRadius) {
245
- return;
246
- }
247
- const modeChanged = next.mode !== this.style.mode;
248
- this.style = next;
249
- if (modeChanged) {
250
- this.redraw();
251
- }
252
- else {
253
- this.group.Element.select('.trace-line').attr('stroke-width', next.lineWidth);
254
- this.zoomDataPoints();
255
- }
256
- }
257
- redraw() {
258
- const group = this.drawTrace(this.data).attr('pointer-events', 'none');
259
- // Replace in place to retain trace ordering beneath the separate marker layer.
260
- this.group.Node.replaceWith(group.Node);
261
- this.group = group;
262
- this.group.Element.style('display', () => (this.visible ? null : 'none'));
263
- }
264
- setVisible(visible) {
265
- this.visible = visible;
266
- this.group.Element.style('display', () => (visible ? null : 'none'));
267
- this.markers.forEach(({ marker }) => {
268
- if (!visible) {
269
- marker.cancelDrag();
270
- }
271
- marker.Element.style('display', () => (visible ? null : 'none'));
272
- });
273
- }
274
- copySamples(values) {
275
- if (!values.length ||
276
- values.some(({ frequencyHz, reflectionCoefficient }) => !Number.isFinite(frequencyHz) ||
277
- frequencyHz < 0 ||
278
- reflectionCoefficient.length !== 2 ||
279
- !reflectionCoefficient.every(Number.isFinite))) {
280
- throw new RangeError('A dataset requires samples with a non-negative finite frequency and two finite coordinates.');
281
- }
282
- return values.map(({ frequencyHz, reflectionCoefficient }) => ({
283
- frequencyHz,
284
- reflectionCoefficient: [reflectionCoefficient[0], reflectionCoefficient[1]],
285
- }));
286
- }
287
- notifyMarker(entry) {
288
- if (this.pendingEvents.has(entry)) {
289
- return;
290
- }
291
- const timer = setTimeout(() => {
292
- this.pendingEvents.delete(entry);
293
- const index = this.markers.indexOf(entry);
294
- if (!this.destroyed && index >= 0) {
295
- this.handler?.(index, entry.selectedPoint);
296
- }
297
- }, 0);
298
- this.pendingEvents.set(entry, timer);
299
- }
300
- cancelEvents() {
301
- this.pendingEvents.forEach((timer) => clearTimeout(timer));
302
- this.pendingEvents.clear();
303
- }
304
- update(values, strategy = 'frequency') {
305
- if (this.destroyed) {
306
- throw new Error('This dataset has been removed.');
307
- }
308
- const samples = this.copySamples(values);
309
- const selectedIndices = this.markers.map((entry) => this.data.indexOf(entry.selectedPoint));
310
- this.cancelEvents();
311
- this.data = samples;
312
- const group = this.drawTrace(samples).attr('pointer-events', 'none');
313
- this.group.Element.remove();
314
- this.group = group;
315
- this.group.Element.style('display', () => (this.visible ? null : 'none'));
316
- this.fgContainer.append(group);
317
- this.zoomDataPoints();
318
- this.markers.forEach((entry, index) => {
319
- if (strategy === 'sample-index') {
320
- entry.selectedPoint = samples[Math.min(selectedIndices[index], samples.length - 1)];
321
- }
322
- else if (strategy === 'reflection') {
323
- entry.selectedPoint = this.findClosestPointTo(entry.selectedPoint.reflectionCoefficient);
324
- }
325
- else {
326
- entry.selectedPoint = samples[this.nearestFrequencyIndex(entry.selectedPoint.frequencyHz)];
327
- }
328
- entry.marker.move(this.scaler.point(entry.selectedPoint.reflectionCoefficient));
329
- this.notifyMarker(entry);
330
- });
331
- }
332
- destroy() {
333
- if (this.destroyed) {
334
- return;
335
- }
336
- this.destroyed = true;
337
- this.cancelEvents();
338
- this.handler = null;
339
- this.markers.forEach(({ marker }) => marker.destroy());
340
- this.markers = [];
341
- this.markerDragHandler = undefined;
342
- this.data = [];
343
- this.group.Element.remove();
344
- }
345
- getMarker(index) {
346
- return this.markers[index];
347
- }
348
- get Markers() {
349
- return this.markers.slice();
350
- }
351
- setMarkerMoveHandler(handler) {
352
- this.handler = handler;
353
- }
354
- findClosestPointTo(p) {
355
- const dist = (p1, p2) => {
356
- const xd = p1[0] - p2[0];
357
- const yd = p1[1] - p2[1];
358
- return Math.hypot(xd, yd);
359
- };
360
- return this.data.reduce((prev, curr) => {
361
- const d1 = dist(p, prev.reflectionCoefficient);
362
- const d2 = dist(p, curr.reflectionCoefficient);
363
- return d1 <= d2 ? prev : curr;
364
- });
365
- }
366
- }