smithkit 0.2.0 → 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 +176 -6
- package/dist/lib/Smith.d.ts +13 -37
- package/dist/lib/Smith.js +82 -282
- 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/appearance/types.js +1 -0
- package/dist/lib/grid/ConstCircles.d.ts +0 -3
- package/dist/lib/grid/ConstCircles.js +6 -20
- package/dist/lib/grid/ConstQCircles.js +1 -1
- package/dist/lib/grid/ConstSwrCircles.js +1 -1
- package/dist/lib/grid/GridDefinitions.d.ts +7 -1
- package/dist/lib/grid/GridDefinitions.js +29 -0
- package/dist/lib/grid/GridLayerControl.d.ts +5 -4
- package/dist/lib/grid/GridLayerControl.js +11 -2
- package/dist/lib/grid/SmithGridLayer.d.ts +7 -1
- package/dist/lib/grid/SmithGridLayer.js +35 -5
- package/dist/lib/index.d.ts +3 -1
- package/dist/lib/interaction/SmithCursor.d.ts +0 -17
- package/dist/lib/interaction/SmithCursor.js +6 -14
- package/dist/lib/layers.d.ts +5 -1
- package/dist/lib/rendering/SvgChartRenderer.d.ts +66 -0
- package/dist/lib/rendering/SvgChartRenderer.js +342 -0
- package/dist/lib/scales/SmithPeripheralScales.js +11 -11
- package/dist/lib/scales/SmithScales.d.ts +10 -3
- package/dist/lib/scales/SmithScales.js +73 -39
- package/dist/lib/svg/ImageExporter.d.ts +18 -0
- package/dist/lib/svg/ImageExporter.js +211 -0
- package/dist/lib/svg/MarkerLegend.d.ts +12 -0
- package/dist/lib/svg/MarkerLegend.js +63 -0
- package/dist/lib/svg/SvgExporter.d.ts +6 -1
- package/dist/lib/svg/SvgExporter.js +50 -5
- package/dist/lib/svg/export.d.ts +36 -0
- package/dist/lib/svg/export.js +1 -0
- package/dist/lib/traces/SmithMarker.js +8 -5
- package/package.json +1 -1
|
@@ -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,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
|
+
}
|
|
@@ -6,7 +6,12 @@ export declare class SvgExporter {
|
|
|
6
6
|
private static readonly nonInherited;
|
|
7
7
|
static chart(source: SVGElement): string;
|
|
8
8
|
/** Preserve the current CSS layout of separate scale SVGs in one SVG document. */
|
|
9
|
-
static scales(container: HTMLElement, axes: readonly SVGSVGElement[]
|
|
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;
|
|
10
15
|
private static bounds;
|
|
11
16
|
private static copy;
|
|
12
17
|
private static size;
|
|
@@ -49,25 +49,70 @@ export class SvgExporter {
|
|
|
49
49
|
static chart(source) {
|
|
50
50
|
const bounds = SvgExporter.bounds(source);
|
|
51
51
|
const copy = SvgExporter.copy(source);
|
|
52
|
+
SvgExporter.background(copy, source);
|
|
52
53
|
SvgExporter.size(copy, bounds.width, bounds.height);
|
|
53
54
|
return SvgExporter.serialize(copy);
|
|
54
55
|
}
|
|
55
56
|
/** Preserve the current CSS layout of separate scale SVGs in one SVG document. */
|
|
56
|
-
static scales(container, axes) {
|
|
57
|
+
static scales(container, axes, customize, caption) {
|
|
57
58
|
const bounds = SvgExporter.bounds(container);
|
|
58
59
|
const root = container.ownerDocument.createElementNS(SvgExporter.namespace, 'svg');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
const lines = [];
|
|
61
|
+
if (caption?.text) {
|
|
62
|
+
const context = document.createElement('canvas').getContext('2d');
|
|
63
|
+
context.font = `14px ${caption.fontFamily}`;
|
|
64
|
+
let line = '';
|
|
65
|
+
for (const character of caption.text) {
|
|
66
|
+
if (character === '\n' ||
|
|
67
|
+
(line && context.measureText(line + character).width > Math.max(1, bounds.width - 32))) {
|
|
68
|
+
lines.push(line);
|
|
69
|
+
line = '';
|
|
70
|
+
}
|
|
71
|
+
if (character !== '\n') {
|
|
72
|
+
line += character;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
lines.push(line);
|
|
76
|
+
}
|
|
77
|
+
const captionHeight = lines.length ? 16 + lines.length * 22 : 0;
|
|
78
|
+
root.setAttribute('viewBox', `0 0 ${bounds.width} ${bounds.height + captionHeight}`);
|
|
79
|
+
SvgExporter.size(root, bounds.width, bounds.height + captionHeight);
|
|
80
|
+
SvgExporter.background(root, container);
|
|
81
|
+
lines.forEach((line, index) => {
|
|
82
|
+
const text = container.ownerDocument.createElementNS(SvgExporter.namespace, 'text');
|
|
83
|
+
text.setAttribute('data-role', 'scale-readout-label');
|
|
84
|
+
text.setAttribute('x', '16');
|
|
85
|
+
text.setAttribute('y', String(22 + index * 22));
|
|
86
|
+
text.setAttribute('fill', caption.color);
|
|
87
|
+
text.setAttribute('font-family', caption.fontFamily);
|
|
88
|
+
text.setAttribute('font-size', '14');
|
|
89
|
+
text.textContent = line;
|
|
90
|
+
root.appendChild(text);
|
|
91
|
+
});
|
|
92
|
+
for (const [index, axis] of axes.entries()) {
|
|
62
93
|
const position = SvgExporter.bounds(axis);
|
|
63
94
|
const copy = SvgExporter.copy(axis);
|
|
64
95
|
copy.setAttribute('x', String(position.x - bounds.x));
|
|
65
|
-
copy.setAttribute('y', String(position.y - bounds.y));
|
|
96
|
+
copy.setAttribute('y', String(position.y - bounds.y + captionHeight));
|
|
66
97
|
SvgExporter.size(copy, position.width, position.height);
|
|
98
|
+
customize?.(copy, index);
|
|
67
99
|
root.appendChild(copy);
|
|
68
100
|
}
|
|
69
101
|
return SvgExporter.serialize(root);
|
|
70
102
|
}
|
|
103
|
+
static background(root, source) {
|
|
104
|
+
const color = source.ownerDocument.defaultView.getComputedStyle(source).backgroundColor;
|
|
105
|
+
if (!color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)') {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const rect = source.ownerDocument.createElementNS(SvgExporter.namespace, 'rect');
|
|
109
|
+
rect.setAttribute('data-export-background', 'true');
|
|
110
|
+
rect.setAttribute('width', '100%');
|
|
111
|
+
rect.setAttribute('height', '100%');
|
|
112
|
+
rect.setAttribute('fill', color);
|
|
113
|
+
rect.setAttribute('stroke', 'none');
|
|
114
|
+
root.insertBefore(rect, root.firstChild);
|
|
115
|
+
}
|
|
71
116
|
static bounds(element) {
|
|
72
117
|
const bounds = element.getBoundingClientRect();
|
|
73
118
|
if (!element.isConnected || bounds.width <= 0 || bounds.height <= 0) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Complex } from '../math/Complex.js';
|
|
2
|
+
import type { SmithScales } from '../scales/SmithScales.js';
|
|
3
|
+
export interface ImageExportOptions {
|
|
4
|
+
/** Output pixels. Default: the snapshot dimensions. With only one dimension, preserve the complete image's aspect ratio. */
|
|
5
|
+
width?: number;
|
|
6
|
+
/** With both dimensions, center and fit the image without stretching or cropping. */
|
|
7
|
+
height?: number;
|
|
8
|
+
/** A concrete CSS color, or transparent. Omit to preserve configured SVG backgrounds. */
|
|
9
|
+
background?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ScaleExportReadout {
|
|
12
|
+
/** Use this reading instead of the live cursor/marker state; null exports empty scales. */
|
|
13
|
+
reflectionCoefficient: Complex | null;
|
|
14
|
+
/** Visible description of the reading, such as trace, marker, and frequency. */
|
|
15
|
+
label?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ScaleImageExportOptions extends ImageExportOptions {
|
|
18
|
+
readout?: ScaleExportReadout;
|
|
19
|
+
}
|
|
20
|
+
export type MarkerLegendField = 'frequency' | 'impedance' | 'admittance' | 'reflectionCoefficient' | 'vswr' | 'returnLoss';
|
|
21
|
+
export interface MarkerLegendOptions {
|
|
22
|
+
/** Default: all markers of visible traces, in trace/marker order. Empty selects none. Unknown IDs throw. */
|
|
23
|
+
markerIds?: readonly string[];
|
|
24
|
+
/** Default: frequency and impedance. Empty keeps only the trace name and marker number. */
|
|
25
|
+
fields?: readonly MarkerLegendField[];
|
|
26
|
+
}
|
|
27
|
+
export interface SmithImageExportOptions extends ImageExportOptions {
|
|
28
|
+
/** Include a footer with the names and colors of visible traces. Default: false. */
|
|
29
|
+
legend?: boolean;
|
|
30
|
+
/** Include a marker legend below the chart/scales, independently of the trace legend. Default: false. */
|
|
31
|
+
markerLegend?: boolean | MarkerLegendOptions;
|
|
32
|
+
/** Include mounted radial scales centered below the chart, preserving their current layout/readings. */
|
|
33
|
+
scales?: SmithScales;
|
|
34
|
+
/** Override readings in the included radial scales without changing the live view. */
|
|
35
|
+
scaleReadout?: ScaleExportReadout;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -53,8 +53,8 @@ export class SmithMarker extends SmithShape {
|
|
|
53
53
|
.attr('pointer-events', 'none')
|
|
54
54
|
.attr('aria-hidden', 'true');
|
|
55
55
|
for (const [stroke, width] of [
|
|
56
|
-
['
|
|
57
|
-
['
|
|
56
|
+
['var(--smithkit-marker-focusHaloColor)', 5],
|
|
57
|
+
['var(--smithkit-marker-focusColor)', 2.5],
|
|
58
58
|
]) {
|
|
59
59
|
this.focusRing
|
|
60
60
|
.append('circle')
|
|
@@ -63,17 +63,20 @@ export class SmithMarker extends SmithShape {
|
|
|
63
63
|
.attr('stroke-width', width)
|
|
64
64
|
.attr('vector-effect', 'non-scaling-stroke');
|
|
65
65
|
}
|
|
66
|
-
this.triangle = g
|
|
66
|
+
this.triangle = g
|
|
67
|
+
.append('polygon')
|
|
68
|
+
.attr('stroke', 'none')
|
|
69
|
+
.attr('fill', 'var(--smithkit-marker-outlineColor)');
|
|
67
70
|
this.inner = g.append('polygon').attr('stroke', 'none').attr('fill', color);
|
|
68
71
|
this.text = g
|
|
69
72
|
.append('text')
|
|
70
73
|
.attr('pointer-events', 'none')
|
|
71
74
|
.attr('aria-hidden', 'true')
|
|
72
|
-
.attr('font-family', '
|
|
75
|
+
.attr('font-family', 'var(--smithkit-fontFamily)')
|
|
73
76
|
.attr('font-weight', 'normal')
|
|
74
77
|
.attr('text-anchor', 'middle')
|
|
75
78
|
.attr('dominant-baseline', 'middle')
|
|
76
|
-
.attr('fill', '
|
|
79
|
+
.attr('fill', 'var(--smithkit-marker-textColor)')
|
|
77
80
|
.text(marker.toString());
|
|
78
81
|
this.zoom(1);
|
|
79
82
|
const drag = d3
|