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.
- package/README.md +291 -42
- package/dist/lib/MarkerMeasurements.d.ts +6 -0
- package/dist/lib/MarkerMeasurements.js +22 -0
- package/dist/lib/Smith.d.ts +18 -35
- package/dist/lib/Smith.js +107 -275
- package/dist/lib/SmithFormatter.d.ts +10 -0
- package/dist/lib/SmithFormatter.js +19 -0
- package/dist/lib/appearance/SvgTheme.d.ts +6 -0
- package/dist/lib/appearance/SvgTheme.js +21 -0
- package/dist/lib/appearance/Theme.d.ts +8 -0
- package/dist/lib/appearance/Theme.js +120 -0
- package/dist/lib/appearance/types.d.ts +55 -0
- package/dist/lib/grid/CircleLayerControl.d.ts +15 -0
- package/dist/lib/grid/CircleLayerControl.js +40 -0
- package/dist/lib/{draw → grid}/ConstCircles.d.ts +4 -9
- package/dist/lib/{draw → grid}/ConstCircles.js +8 -22
- package/dist/lib/{draw → grid}/ConstQCircles.d.ts +2 -2
- package/dist/lib/{draw → grid}/ConstQCircles.js +4 -4
- package/dist/lib/{draw → grid}/ConstSwrCircles.d.ts +2 -2
- package/dist/lib/{draw → grid}/ConstSwrCircles.js +4 -4
- package/dist/lib/grid/GridDefinitions.d.ts +18 -0
- package/dist/lib/grid/GridDefinitions.js +167 -0
- package/dist/lib/grid/GridGeometry.d.ts +26 -0
- package/dist/lib/grid/GridGeometry.js +58 -0
- package/dist/lib/grid/GridLabels.d.ts +8 -0
- package/dist/lib/grid/GridLabels.js +45 -0
- package/dist/lib/grid/GridLayerControl.d.ts +13 -0
- package/dist/lib/grid/GridLayerControl.js +48 -0
- package/dist/lib/grid/LayerControl.d.ts +6 -0
- package/dist/lib/grid/LayerControl.js +16 -0
- package/dist/lib/grid/SmithGridLayer.d.ts +26 -0
- package/dist/lib/grid/SmithGridLayer.js +95 -0
- package/dist/lib/grid/types.d.ts +1 -1
- package/dist/lib/index.d.ts +11 -11
- package/dist/lib/index.js +5 -7
- package/dist/lib/{draw → interaction}/SmithCursor.d.ts +3 -20
- package/dist/lib/{draw → interaction}/SmithCursor.js +12 -20
- package/dist/lib/io/Touchstone.d.ts +10 -0
- package/dist/lib/io/Touchstone.js +75 -0
- package/dist/lib/layers.d.ts +7 -9
- package/dist/lib/layers.js +1 -73
- package/dist/lib/{complex → math}/Complex.d.ts +1 -0
- package/dist/lib/{complex → math}/Complex.js +1 -1
- package/dist/lib/math/geometry.d.ts +14 -0
- package/dist/lib/measurements.d.ts +2 -4
- package/dist/lib/measurements.js +1 -19
- package/dist/lib/rendering/SvgChartRenderer.d.ts +66 -0
- package/dist/lib/rendering/SvgChartRenderer.js +342 -0
- package/dist/lib/rf/RfCalculations.d.ts +65 -0
- package/dist/lib/rf/RfCalculations.js +161 -0
- package/dist/lib/{SmithConstantCircle.d.ts → rf/SmithConstantCircle.d.ts} +2 -3
- package/dist/lib/{SmithConstantCircle.js → rf/SmithConstantCircle.js} +3 -4
- package/dist/lib/scales/PeripheralScaleDefinitions.d.ts +16 -0
- package/dist/lib/scales/PeripheralScaleDefinitions.js +54 -0
- package/dist/lib/scales/RadialScaleDefinitions.d.ts +15 -0
- package/dist/lib/scales/RadialScaleDefinitions.js +112 -0
- package/dist/lib/scales/SmithPeripheralScales.d.ts +4 -2
- package/dist/lib/scales/SmithPeripheralScales.js +55 -35
- package/dist/lib/scales/SmithScales.d.ts +11 -2
- package/dist/lib/scales/SmithScales.js +78 -38
- package/dist/lib/svg/ImageExporter.d.ts +18 -0
- package/dist/lib/svg/ImageExporter.js +211 -0
- package/dist/lib/svg/LabelLayout.d.ts +17 -0
- package/dist/lib/svg/LabelLayout.js +145 -0
- package/dist/lib/svg/MarkerLegend.d.ts +12 -0
- package/dist/lib/svg/MarkerLegend.js +63 -0
- package/dist/lib/{draw → svg}/SmithArc.d.ts +1 -1
- package/dist/lib/{draw → svg}/SmithCircle.d.ts +1 -1
- package/dist/lib/{draw → svg}/SmithLine.d.ts +1 -1
- package/dist/lib/{draw → svg}/SmithScaler.d.ts +1 -4
- package/dist/lib/{draw → svg}/SmithText.d.ts +1 -1
- package/dist/lib/svg/SvgExporter.d.ts +19 -0
- package/dist/lib/svg/SvgExporter.js +182 -0
- package/dist/lib/svg/export.d.ts +36 -0
- package/dist/lib/{draw → traces}/SmithData.d.ts +24 -35
- package/dist/lib/traces/SmithData.js +195 -0
- package/dist/lib/{draw → traces}/SmithMarker.d.ts +14 -3
- package/dist/lib/traces/SmithMarker.js +240 -0
- package/dist/lib/traces/TraceModel.d.ts +31 -0
- package/dist/lib/traces/TraceModel.js +123 -0
- package/dist/lib/traces/TraceRenderer.d.ts +33 -0
- package/dist/lib/traces/TraceRenderer.js +152 -0
- package/package.json +4 -1
- package/dist/lib/SmithArcsDefs.d.ts +0 -10
- package/dist/lib/SmithArcsDefs.js +0 -10
- package/dist/lib/components.d.ts +0 -14
- package/dist/lib/components.js +0 -26
- package/dist/lib/draw/SmithData.js +0 -366
- package/dist/lib/draw/SmithGridLayer.d.ts +0 -19
- package/dist/lib/draw/SmithGridLayer.js +0 -48
- package/dist/lib/draw/SmithMarker.js +0 -122
- package/dist/lib/formatting.d.ts +0 -7
- package/dist/lib/formatting.js +0 -16
- package/dist/lib/grid/definitions.d.ts +0 -3
- package/dist/lib/grid/definitions.js +0 -123
- package/dist/lib/grid/geometry.d.ts +0 -20
- package/dist/lib/grid/geometry.js +0 -55
- package/dist/lib/grid/labels.d.ts +0 -3
- package/dist/lib/grid/labels.js +0 -41
- package/dist/lib/io/touchstone.d.ts +0 -7
- package/dist/lib/io/touchstone.js +0 -72
- package/dist/lib/math/complexRatio.d.ts +0 -3
- package/dist/lib/math/complexRatio.js +0 -30
- package/dist/lib/renormalization.d.ts +0 -10
- package/dist/lib/renormalization.js +0 -62
- package/dist/lib/rf.d.ts +0 -35
- package/dist/lib/rf.js +0 -74
- package/dist/lib/scales/peripheralScales.d.ts +0 -12
- package/dist/lib/scales/peripheralScales.js +0 -49
- package/dist/lib/scales/radialScales.d.ts +0 -12
- package/dist/lib/scales/radialScales.js +0 -109
- package/dist/lib/shapes/Arc.d.ts +0 -6
- package/dist/lib/shapes/Circle.d.ts +0 -5
- package/dist/lib/shapes/Line.d.ts +0 -5
- package/dist/lib/shapes/Point.d.ts +0 -1
- package/dist/lib/shapes/Point.js +0 -1
- /package/dist/lib/{draw/SmithDrawOptions.js → appearance/types.js} +0 -0
- /package/dist/lib/{draw → grid}/ConstConductance.d.ts +0 -0
- /package/dist/lib/{draw → grid}/ConstConductance.js +0 -0
- /package/dist/lib/{draw → grid}/ConstReactance.d.ts +0 -0
- /package/dist/lib/{draw → grid}/ConstReactance.js +0 -0
- /package/dist/lib/{draw → grid}/ConstResistance.d.ts +0 -0
- /package/dist/lib/{draw → grid}/ConstResistance.js +0 -0
- /package/dist/lib/{draw → grid}/ConstSusceptance.d.ts +0 -0
- /package/dist/lib/{draw → grid}/ConstSusceptance.js +0 -0
- /package/dist/lib/{arcs → grid}/Tick.d.ts +0 -0
- /package/dist/lib/{arcs → grid}/Tick.js +0 -0
- /package/dist/lib/{draw → interaction}/MouseGesture.d.ts +0 -0
- /package/dist/lib/{draw → interaction}/MouseGesture.js +0 -0
- /package/dist/lib/{shapes/Arc.js → math/geometry.js} +0 -0
- /package/dist/lib/{draw → svg}/SmithArc.js +0 -0
- /package/dist/lib/{draw → svg}/SmithCircle.js +0 -0
- /package/dist/lib/{draw → svg}/SmithDrawOptions.d.ts +0 -0
- /package/dist/lib/{shapes/Circle.js → svg/SmithDrawOptions.js} +0 -0
- /package/dist/lib/{draw → svg}/SmithGroup.d.ts +0 -0
- /package/dist/lib/{draw → svg}/SmithGroup.js +0 -0
- /package/dist/lib/{draw → svg}/SmithLine.js +0 -0
- /package/dist/lib/{draw → svg}/SmithPath.d.ts +0 -0
- /package/dist/lib/{draw → svg}/SmithPath.js +0 -0
- /package/dist/lib/{draw → svg}/SmithScaler.js +0 -0
- /package/dist/lib/{draw → svg}/SmithShape.d.ts +0 -0
- /package/dist/lib/{draw → svg}/SmithShape.js +0 -0
- /package/dist/lib/{draw → svg}/SmithSvg.d.ts +0 -0
- /package/dist/lib/{draw → svg}/SmithSvg.js +0 -0
- /package/dist/lib/{draw → svg}/SmithText.js +0 -0
- /package/dist/lib/{shapes/Line.js → svg/export.js} +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** Compose SVG snapshots and rasterize PNG reports without changing live chart nodes. */
|
|
2
|
+
export class ImageExporter {
|
|
3
|
+
constructor() { }
|
|
4
|
+
static layout(sources, options, legend) {
|
|
5
|
+
const { width: requestedWidth, height: requestedHeight, background } = options;
|
|
6
|
+
for (const value of [requestedWidth, requestedHeight]) {
|
|
7
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 1 || value > 8192)) {
|
|
8
|
+
throw new RangeError('Export dimensions must be integers from 1 to 8192 pixels.');
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (background !== undefined &&
|
|
12
|
+
(!CSS.supports('color', background) ||
|
|
13
|
+
/\b(var|currentcolor|inherit|initial|unset|revert|light-dark)\b/i.test(background))) {
|
|
14
|
+
throw new RangeError('Export background must be a concrete CSS color or transparent.');
|
|
15
|
+
}
|
|
16
|
+
const snapshots = sources.map((source) => {
|
|
17
|
+
const svg = new DOMParser().parseFromString(source, 'image/svg+xml').documentElement;
|
|
18
|
+
if (background !== undefined) {
|
|
19
|
+
svg.querySelectorAll('[data-export-background]').forEach((node) => node.remove());
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
svg,
|
|
23
|
+
width: Number(svg.getAttribute('width')),
|
|
24
|
+
height: Number(svg.getAttribute('height')),
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
const gap = snapshots.length > 1 ? 24 : 0;
|
|
28
|
+
const contentWidth = Math.max(...snapshots.map((source) => source.width));
|
|
29
|
+
const contentHeight = snapshots.reduce((sum, source) => sum + source.height, 0) + gap * (snapshots.length - 1);
|
|
30
|
+
const canvas = document.createElement('canvas');
|
|
31
|
+
const context = canvas.getContext('2d');
|
|
32
|
+
if (!context) {
|
|
33
|
+
throw new Error('Image export requires a Canvas 2D context.');
|
|
34
|
+
}
|
|
35
|
+
context.font = `14px ${legend?.fontFamily ?? 'sans-serif'}`;
|
|
36
|
+
const rows = (legend?.entries ?? []).flatMap((entry) => {
|
|
37
|
+
const lines = ImageExporter.wrap(entry.name, Math.max(1, contentWidth - 64), context);
|
|
38
|
+
return lines.map((text, index) => ({ text, color: entry.color, swatch: index === 0 }));
|
|
39
|
+
});
|
|
40
|
+
const legendHeight = rows.length ? 32 + rows.length * 22 : 0;
|
|
41
|
+
const naturalHeight = contentHeight + legendHeight;
|
|
42
|
+
const width = requestedWidth ??
|
|
43
|
+
Math.max(1, Math.round(requestedHeight === undefined
|
|
44
|
+
? contentWidth
|
|
45
|
+
: (requestedHeight * contentWidth) / naturalHeight));
|
|
46
|
+
const height = requestedHeight ?? Math.max(1, Math.round((width * naturalHeight) / contentWidth));
|
|
47
|
+
if (width > 8192 || height > 8192 || width * height > 33_554_432) {
|
|
48
|
+
throw new RangeError('Image output must not exceed 8192 pixels per side or 32 megapixels.');
|
|
49
|
+
}
|
|
50
|
+
const scale = Math.min(width / contentWidth, height / naturalHeight);
|
|
51
|
+
return {
|
|
52
|
+
snapshots,
|
|
53
|
+
gap,
|
|
54
|
+
contentWidth,
|
|
55
|
+
contentHeight,
|
|
56
|
+
context,
|
|
57
|
+
canvas,
|
|
58
|
+
rows,
|
|
59
|
+
legendHeight,
|
|
60
|
+
width,
|
|
61
|
+
height,
|
|
62
|
+
scale,
|
|
63
|
+
naturalHeight,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
static async png(sources, options, legend) {
|
|
67
|
+
const snapshotOptions = { ...options };
|
|
68
|
+
await document.fonts.ready;
|
|
69
|
+
const { snapshots, gap, contentWidth, contentHeight, context, canvas, rows, legendHeight, width, height, scale, naturalHeight, } = ImageExporter.layout(sources, snapshotOptions, legend);
|
|
70
|
+
const background = snapshotOptions.background;
|
|
71
|
+
canvas.width = width;
|
|
72
|
+
canvas.height = height;
|
|
73
|
+
// Explicit backgrounds replace component backgrounds; omission preserves them.
|
|
74
|
+
if (background !== undefined) {
|
|
75
|
+
context.fillStyle = background;
|
|
76
|
+
context.fillRect(0, 0, width, height);
|
|
77
|
+
}
|
|
78
|
+
context.translate((width - contentWidth * scale) / 2, (height - naturalHeight * scale) / 2);
|
|
79
|
+
context.scale(scale, scale);
|
|
80
|
+
let y = 0;
|
|
81
|
+
for (const source of snapshots) {
|
|
82
|
+
const url = URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(source.svg)], {
|
|
83
|
+
type: 'image/svg+xml;charset=utf-8',
|
|
84
|
+
}));
|
|
85
|
+
try {
|
|
86
|
+
const image = new Image();
|
|
87
|
+
image.src = url;
|
|
88
|
+
await image.decode();
|
|
89
|
+
context.drawImage(image, (contentWidth - source.width) / 2, y, source.width, source.height);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
URL.revokeObjectURL(url);
|
|
93
|
+
}
|
|
94
|
+
y += source.height + gap;
|
|
95
|
+
}
|
|
96
|
+
if (legend && rows.length) {
|
|
97
|
+
// Use the chart's own background for the footer unless explicitly overridden.
|
|
98
|
+
const chartBackground = snapshots[0].svg
|
|
99
|
+
.querySelector('[data-export-background]')
|
|
100
|
+
?.getAttribute('fill');
|
|
101
|
+
if (background === undefined && chartBackground) {
|
|
102
|
+
context.fillStyle = chartBackground;
|
|
103
|
+
context.fillRect(0, contentHeight, contentWidth, legendHeight);
|
|
104
|
+
}
|
|
105
|
+
context.font = `14px ${legend.fontFamily}`;
|
|
106
|
+
context.textBaseline = 'middle';
|
|
107
|
+
for (const [index, row] of rows.entries()) {
|
|
108
|
+
const y = contentHeight + 24 + index * 22;
|
|
109
|
+
if (row.swatch) {
|
|
110
|
+
context.strokeStyle = row.color;
|
|
111
|
+
context.lineWidth = 3;
|
|
112
|
+
context.beginPath();
|
|
113
|
+
context.moveTo(16, y);
|
|
114
|
+
context.lineTo(40, y);
|
|
115
|
+
context.stroke();
|
|
116
|
+
}
|
|
117
|
+
context.fillStyle = legend.textColor;
|
|
118
|
+
context.fillText(row.text, 48, y);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
canvas.toBlob((blob) => {
|
|
123
|
+
if (blob) {
|
|
124
|
+
resolve(blob);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
reject(new Error('The browser could not encode the PNG image.'));
|
|
128
|
+
}
|
|
129
|
+
}, 'image/png');
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
static svg(sources, options, legend) {
|
|
133
|
+
const { snapshots, gap, contentWidth, contentHeight, rows, legendHeight, width, height, scale, naturalHeight, } = ImageExporter.layout(sources, options, legend);
|
|
134
|
+
const namespace = 'http://www.w3.org/2000/svg';
|
|
135
|
+
const root = document.createElementNS(namespace, 'svg');
|
|
136
|
+
const left = -(width / scale - contentWidth) / 2;
|
|
137
|
+
const top = -(height / scale - naturalHeight) / 2;
|
|
138
|
+
root.setAttribute('width', String(width));
|
|
139
|
+
root.setAttribute('height', String(height));
|
|
140
|
+
root.setAttribute('viewBox', `${left} ${top} ${width / scale} ${height / scale}`);
|
|
141
|
+
const append = (tag, attributes) => {
|
|
142
|
+
const node = document.createElementNS(namespace, tag);
|
|
143
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
144
|
+
node.setAttribute(name, String(value));
|
|
145
|
+
}
|
|
146
|
+
root.appendChild(node);
|
|
147
|
+
return node;
|
|
148
|
+
};
|
|
149
|
+
if (options.background !== undefined) {
|
|
150
|
+
append('rect', {
|
|
151
|
+
x: left,
|
|
152
|
+
y: top,
|
|
153
|
+
width: width / scale,
|
|
154
|
+
height: height / scale,
|
|
155
|
+
fill: options.background,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
let y = 0;
|
|
159
|
+
for (const source of snapshots) {
|
|
160
|
+
source.svg.setAttribute('x', String((contentWidth - source.width) / 2));
|
|
161
|
+
source.svg.setAttribute('y', String(y));
|
|
162
|
+
root.appendChild(source.svg);
|
|
163
|
+
y += source.height + gap;
|
|
164
|
+
}
|
|
165
|
+
if (legend && rows.length) {
|
|
166
|
+
const chartBackground = snapshots[0].svg
|
|
167
|
+
.querySelector('[data-export-background]')
|
|
168
|
+
?.getAttribute('fill');
|
|
169
|
+
if (options.background === undefined && chartBackground) {
|
|
170
|
+
append('rect', {
|
|
171
|
+
x: 0,
|
|
172
|
+
y: contentHeight,
|
|
173
|
+
width: contentWidth,
|
|
174
|
+
height: legendHeight,
|
|
175
|
+
fill: chartBackground,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
for (const [index, row] of rows.entries()) {
|
|
179
|
+
const y = contentHeight + 24 + index * 22;
|
|
180
|
+
if (row.swatch) {
|
|
181
|
+
append('line', { x1: 16, x2: 40, y1: y, y2: y, stroke: row.color, 'stroke-width': 3 });
|
|
182
|
+
}
|
|
183
|
+
const text = append('text', {
|
|
184
|
+
x: 48,
|
|
185
|
+
y,
|
|
186
|
+
fill: legend.textColor,
|
|
187
|
+
'font-size': 14,
|
|
188
|
+
'font-family': legend.fontFamily,
|
|
189
|
+
'dominant-baseline': 'central',
|
|
190
|
+
});
|
|
191
|
+
text.textContent = row.text;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return new XMLSerializer().serializeToString(root);
|
|
195
|
+
}
|
|
196
|
+
static wrap(text, width, context) {
|
|
197
|
+
const lines = [];
|
|
198
|
+
let line = '';
|
|
199
|
+
for (const character of text) {
|
|
200
|
+
if (character === '\n' || (line && context.measureText(line + character).width > width)) {
|
|
201
|
+
lines.push(line);
|
|
202
|
+
line = '';
|
|
203
|
+
}
|
|
204
|
+
if (character !== '\n') {
|
|
205
|
+
line += character;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
lines.push(line);
|
|
209
|
+
return lines;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Responsive label placement measured in the default view, independent of user zoom. */
|
|
2
|
+
export declare class LabelLayout {
|
|
3
|
+
private readonly viewport;
|
|
4
|
+
private readonly container;
|
|
5
|
+
private readonly baseView;
|
|
6
|
+
private width;
|
|
7
|
+
private height;
|
|
8
|
+
private readonly resizedFonts;
|
|
9
|
+
constructor(viewport: SVGSVGElement, container: SVGGElement, baseView: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
k: number;
|
|
13
|
+
});
|
|
14
|
+
update(force?: boolean): void;
|
|
15
|
+
private static transformBox;
|
|
16
|
+
private static overlaps;
|
|
17
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Responsive label placement measured in the default view, independent of user zoom. */
|
|
2
|
+
export class LabelLayout {
|
|
3
|
+
viewport;
|
|
4
|
+
container;
|
|
5
|
+
baseView;
|
|
6
|
+
width = 0;
|
|
7
|
+
height = 0;
|
|
8
|
+
resizedFonts = new Map();
|
|
9
|
+
constructor(viewport, container, baseView) {
|
|
10
|
+
this.viewport = viewport;
|
|
11
|
+
this.container = container;
|
|
12
|
+
this.baseView = baseView;
|
|
13
|
+
}
|
|
14
|
+
update(force = false) {
|
|
15
|
+
const viewport = this.viewport.getBoundingClientRect();
|
|
16
|
+
if (!this.viewport.isConnected || viewport.width <= 0 || viewport.height <= 0) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!force && viewport.width === this.width && viewport.height === this.height) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.width = viewport.width;
|
|
23
|
+
this.height = viewport.height;
|
|
24
|
+
const rootMatrix = this.viewport.getScreenCTM();
|
|
25
|
+
const containerMatrix = this.container.getScreenCTM();
|
|
26
|
+
if (!rootMatrix || !containerMatrix) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const reference = rootMatrix.translate(this.baseView.x, this.baseView.y).scale(this.baseView.k);
|
|
30
|
+
const normalize = reference.multiply(containerMatrix.inverse());
|
|
31
|
+
// Peripheral rulers retain their complete, fixed-scale lettering. Only grid
|
|
32
|
+
// labels participate in thinning and minimum-font-size adjustments.
|
|
33
|
+
const labels = [...this.container.querySelectorAll('text')].filter((label) => !label.closest('[data-label-layer=peripheral-scales]'));
|
|
34
|
+
const view = this.viewport.ownerDocument.defaultView;
|
|
35
|
+
// Restore only properties owned by this layout before measuring the base styling.
|
|
36
|
+
for (const label of labels) {
|
|
37
|
+
if (label.hasAttribute('data-label-hidden')) {
|
|
38
|
+
label.removeAttribute('visibility');
|
|
39
|
+
label.removeAttribute('data-label-hidden');
|
|
40
|
+
}
|
|
41
|
+
const previousFont = this.resizedFonts.get(label);
|
|
42
|
+
if (previousFont !== undefined) {
|
|
43
|
+
if (previousFont) {
|
|
44
|
+
label.style.fontSize = previousFont;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
label.style.removeProperty('font-size');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
this.resizedFonts.clear();
|
|
52
|
+
const styles = new Map();
|
|
53
|
+
const style = (element) => {
|
|
54
|
+
let result = styles.get(element);
|
|
55
|
+
if (!result) {
|
|
56
|
+
result = view.getComputedStyle(element);
|
|
57
|
+
styles.set(element, result);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
const candidates = labels.flatMap((label, order) => {
|
|
62
|
+
for (let parent = label; parent; parent = parent.parentElement) {
|
|
63
|
+
const computed = style(parent);
|
|
64
|
+
if (computed.display === 'none' ||
|
|
65
|
+
computed.visibility === 'hidden' ||
|
|
66
|
+
Number(computed.opacity) === 0) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
if (parent === this.viewport) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const screenMatrix = label.getScreenCTM();
|
|
74
|
+
const matrix = screenMatrix ? normalize.multiply(screenMatrix) : null;
|
|
75
|
+
const scale = matrix ? Math.hypot(matrix.a, matrix.b) : 0;
|
|
76
|
+
if (scale <= 0) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
label,
|
|
82
|
+
order,
|
|
83
|
+
scale,
|
|
84
|
+
matrix: matrix,
|
|
85
|
+
baseSize: parseFloat(style(label).fontSize),
|
|
86
|
+
priority: Number(label.dataset.labelPriority ?? 0),
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
});
|
|
90
|
+
// Batch font changes before reading bounds to avoid alternating layout reads/writes.
|
|
91
|
+
for (const { label, scale, baseSize } of candidates) {
|
|
92
|
+
if (Number.isFinite(baseSize) && baseSize * scale < 9) {
|
|
93
|
+
this.resizedFonts.set(label, label.style.fontSize);
|
|
94
|
+
label.style.fontSize = `${9 / scale}px`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const measured = candidates.map((entry) => ({
|
|
98
|
+
...entry,
|
|
99
|
+
box: LabelLayout.transformBox(entry.label.getBBox(), entry.matrix),
|
|
100
|
+
}));
|
|
101
|
+
measured.sort((a, b) => b.priority - a.priority || a.order - b.order);
|
|
102
|
+
const occupied = [];
|
|
103
|
+
const hidden = [];
|
|
104
|
+
for (const { label, box } of measured) {
|
|
105
|
+
const outside = box.right <= box.left ||
|
|
106
|
+
box.bottom <= box.top ||
|
|
107
|
+
box.left < viewport.left + 1 ||
|
|
108
|
+
box.right > viewport.right - 1 ||
|
|
109
|
+
box.top < viewport.top + 1 ||
|
|
110
|
+
box.bottom > viewport.bottom - 1;
|
|
111
|
+
const collision = occupied.some((other) => LabelLayout.overlaps(box, other));
|
|
112
|
+
if (outside || collision) {
|
|
113
|
+
hidden.push(label);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
occupied.push(box);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const label of hidden) {
|
|
120
|
+
label.setAttribute('visibility', 'hidden');
|
|
121
|
+
label.setAttribute('data-label-hidden', 'true');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
static transformBox(box, matrix) {
|
|
125
|
+
const points = [
|
|
126
|
+
new DOMPoint(box.x, box.y),
|
|
127
|
+
new DOMPoint(box.x + box.width, box.y),
|
|
128
|
+
new DOMPoint(box.x, box.y + box.height),
|
|
129
|
+
new DOMPoint(box.x + box.width, box.y + box.height),
|
|
130
|
+
].map((point) => point.matrixTransform(matrix));
|
|
131
|
+
return {
|
|
132
|
+
left: Math.min(...points.map((point) => point.x)),
|
|
133
|
+
right: Math.max(...points.map((point) => point.x)),
|
|
134
|
+
top: Math.min(...points.map((point) => point.y)),
|
|
135
|
+
bottom: Math.max(...points.map((point) => point.y)),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
static overlaps(a, b) {
|
|
139
|
+
const gap = 2;
|
|
140
|
+
return (a.left < b.right + gap &&
|
|
141
|
+
a.right + gap > b.left &&
|
|
142
|
+
a.top < b.bottom + gap &&
|
|
143
|
+
a.bottom + gap > b.top);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MarkerSnapshot, TraceInfo } from '../measurements.js';
|
|
2
|
+
import type { MarkerLegendOptions } from './export.js';
|
|
3
|
+
/** Format detached marker readings for the shared SVG/PNG report footer. */
|
|
4
|
+
export declare class MarkerLegend {
|
|
5
|
+
private constructor();
|
|
6
|
+
static entries(traces: readonly TraceInfo[], reading: (id: string) => MarkerSnapshot, options: true | MarkerLegendOptions): {
|
|
7
|
+
name: string;
|
|
8
|
+
color: string;
|
|
9
|
+
}[];
|
|
10
|
+
private static field;
|
|
11
|
+
private static number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SmithFormatter } from '../SmithFormatter.js';
|
|
2
|
+
/** Format detached marker readings for the shared SVG/PNG report footer. */
|
|
3
|
+
export class MarkerLegend {
|
|
4
|
+
constructor() { }
|
|
5
|
+
static entries(traces, reading, options) {
|
|
6
|
+
const { markerIds, fields = ['frequency', 'impedance'] } = options === true ? {} : options;
|
|
7
|
+
const knownIds = new Set(traces.flatMap((trace) => trace.markers.map((marker) => marker.id)));
|
|
8
|
+
if (markerIds?.some((id) => !knownIds.has(id))) {
|
|
9
|
+
throw new RangeError('Marker legend contains an unknown marker ID.');
|
|
10
|
+
}
|
|
11
|
+
const available = [
|
|
12
|
+
'frequency',
|
|
13
|
+
'impedance',
|
|
14
|
+
'admittance',
|
|
15
|
+
'reflectionCoefficient',
|
|
16
|
+
'vswr',
|
|
17
|
+
'returnLoss',
|
|
18
|
+
];
|
|
19
|
+
if (fields.some((field) => !available.includes(field))) {
|
|
20
|
+
throw new RangeError('Marker legend contains an unknown field.');
|
|
21
|
+
}
|
|
22
|
+
const selected = markerIds === undefined ? undefined : new Set(markerIds);
|
|
23
|
+
return traces
|
|
24
|
+
.filter((trace) => trace.visible)
|
|
25
|
+
.flatMap((trace) => trace.markers
|
|
26
|
+
.filter((marker) => selected === undefined || selected.has(marker.id))
|
|
27
|
+
.map((marker) => {
|
|
28
|
+
const snapshot = reading(marker.id);
|
|
29
|
+
return {
|
|
30
|
+
color: trace.color,
|
|
31
|
+
name: [
|
|
32
|
+
`${trace.name} · Marker ${marker.number}`,
|
|
33
|
+
...[...new Set(fields)].map((field) => MarkerLegend.field(snapshot, field)),
|
|
34
|
+
].join(' · '),
|
|
35
|
+
};
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
static field(marker, field) {
|
|
39
|
+
switch (field) {
|
|
40
|
+
case 'frequency':
|
|
41
|
+
return `f = ${SmithFormatter.number(marker.frequencyHz)}Hz`;
|
|
42
|
+
case 'impedance':
|
|
43
|
+
return `Z = ${marker.impedanceOhms?.toString(3) ?? '∞'} Ω`;
|
|
44
|
+
case 'admittance':
|
|
45
|
+
return `Y = ${marker.admittanceSiemens?.mul(1000).toString(3) ?? '∞'} mS`;
|
|
46
|
+
case 'reflectionCoefficient':
|
|
47
|
+
return `Γ = ${marker.reflectionCoefficient.toString(3)}`;
|
|
48
|
+
case 'vswr':
|
|
49
|
+
return `VSWR = ${MarkerLegend.number(marker.vswr)}${marker.vswr === undefined ? '' : ' : 1'}`;
|
|
50
|
+
case 'returnLoss':
|
|
51
|
+
return `Return loss = ${MarkerLegend.number(marker.returnLossDb)} dB`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
static number(value) {
|
|
55
|
+
if (value === undefined || Number.isNaN(value)) {
|
|
56
|
+
return '—';
|
|
57
|
+
}
|
|
58
|
+
if (!Number.isFinite(value)) {
|
|
59
|
+
return value < 0 ? '−∞' : '∞';
|
|
60
|
+
}
|
|
61
|
+
return String(Number(value.toPrecision(4)));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import * as d3 from 'd3';
|
|
2
|
-
import { Circle } from '../
|
|
3
|
-
import { Point } from '../shapes/Point.js';
|
|
4
|
-
import { Line } from '../shapes/Line.js';
|
|
5
|
-
import { Arc } from '../shapes/Arc.js';
|
|
2
|
+
import type { Circle, Point, Line, Arc } from '../math/geometry.js';
|
|
6
3
|
export declare class SmithScaler {
|
|
7
4
|
readonly x: d3.ScaleLinear<number, number>;
|
|
8
5
|
readonly y: d3.ScaleLinear<number, number>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Captures rendered SVG with presentation styles resolved in its source document. */
|
|
2
|
+
export declare class SvgExporter {
|
|
3
|
+
private constructor();
|
|
4
|
+
private static readonly namespace;
|
|
5
|
+
private static readonly properties;
|
|
6
|
+
private static readonly nonInherited;
|
|
7
|
+
static chart(source: SVGElement): string;
|
|
8
|
+
/** Preserve the current CSS layout of separate scale SVGs in one SVG document. */
|
|
9
|
+
static scales(container: HTMLElement, axes: readonly SVGSVGElement[], customize?: (copy: SVGElement, index: number) => void, caption?: {
|
|
10
|
+
text: string;
|
|
11
|
+
color: string;
|
|
12
|
+
fontFamily: string;
|
|
13
|
+
}): string;
|
|
14
|
+
private static background;
|
|
15
|
+
private static bounds;
|
|
16
|
+
private static copy;
|
|
17
|
+
private static size;
|
|
18
|
+
private static serialize;
|
|
19
|
+
}
|