react-cheminfo 0.32.0 → 0.34.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 +1 -1
- package/lib/axis/core/axisMarks.d.ts +54 -0
- package/lib/axis/core/axisMarks.d.ts.map +1 -0
- package/lib/axis/core/axisMarks.js +69 -0
- package/lib/axis/core/axisMarks.js.map +1 -0
- package/lib/axis/core/axisScale.d.ts +31 -0
- package/lib/axis/core/axisScale.d.ts.map +1 -0
- package/lib/axis/core/axisScale.js +31 -0
- package/lib/axis/core/axisScale.js.map +1 -0
- package/lib/axis/core/index.d.ts +16 -0
- package/lib/axis/core/index.d.ts.map +1 -0
- package/lib/axis/core/index.js +13 -0
- package/lib/axis/core/index.js.map +1 -0
- package/lib/axis/core/logAxisBounds.d.ts +78 -0
- package/lib/axis/core/logAxisBounds.d.ts.map +1 -0
- package/lib/axis/core/logAxisBounds.js +129 -0
- package/lib/axis/core/logAxisBounds.js.map +1 -0
- package/lib/axis/ui/AxisScaleControl.d.ts +66 -0
- package/lib/axis/ui/AxisScaleControl.d.ts.map +1 -0
- package/lib/axis/ui/AxisScaleControl.js +26 -0
- package/lib/axis/ui/AxisScaleControl.js.map +1 -0
- package/lib/axis/ui/index.d.ts +6 -0
- package/lib/axis/ui/index.d.ts.map +1 -0
- package/lib/axis/ui/index.js +5 -0
- package/lib/axis/ui/index.js.map +1 -0
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/delimited/core/index.d.ts +2 -0
- package/lib/delimited/core/index.d.ts.map +1 -1
- package/lib/delimited/core/index.js +1 -0
- package/lib/delimited/core/index.js.map +1 -1
- package/lib/delimited/core/tableRows.d.ts +49 -0
- package/lib/delimited/core/tableRows.d.ts.map +1 -0
- package/lib/delimited/core/tableRows.js +63 -0
- package/lib/delimited/core/tableRows.js.map +1 -0
- package/lib/delimited/ui/DelimitedTextPanel.d.ts +9 -0
- package/lib/delimited/ui/DelimitedTextPanel.d.ts.map +1 -1
- package/lib/delimited/ui/DelimitedTextPanel.js +3 -2
- package/lib/delimited/ui/DelimitedTextPanel.js.map +1 -1
- package/lib/delimited/ui/TableDataButton.d.ts +96 -0
- package/lib/delimited/ui/TableDataButton.d.ts.map +1 -0
- package/lib/delimited/ui/TableDataButton.js +39 -0
- package/lib/delimited/ui/TableDataButton.js.map +1 -0
- package/lib/delimited/ui/index.d.ts +2 -0
- package/lib/delimited/ui/index.d.ts.map +1 -1
- package/lib/delimited/ui/index.js +1 -0
- package/lib/delimited/ui/index.js.map +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts +2 -2
- package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
- package/lib/i18n/core/chromeCatalog.js +10 -4
- package/lib/i18n/core/chromeCatalog.js.map +1 -1
- package/lib/i18n/core/index.d.ts +2 -0
- package/lib/i18n/core/index.d.ts.map +1 -1
- package/lib/i18n/core/index.js +1 -0
- package/lib/i18n/core/index.js.map +1 -1
- package/lib/i18n/core/languages.d.ts +4 -4
- package/lib/i18n/core/languages.d.ts.map +1 -1
- package/lib/i18n/core/languages.js +5 -4
- package/lib/i18n/core/languages.js.map +1 -1
- package/lib/i18n/core/prose.d.ts +32 -0
- package/lib/i18n/core/prose.d.ts.map +1 -0
- package/lib/i18n/core/prose.js +45 -0
- package/lib/i18n/core/prose.js.map +1 -0
- package/lib/i18n/ui/Prose.d.ts +28 -0
- package/lib/i18n/ui/Prose.d.ts.map +1 -0
- package/lib/i18n/ui/Prose.js +14 -0
- package/lib/i18n/ui/Prose.js.map +1 -0
- package/lib/i18n/ui/index.d.ts +2 -0
- package/lib/i18n/ui/index.d.ts.map +1 -1
- package/lib/i18n/ui/index.js +1 -0
- package/lib/i18n/ui/index.js.map +1 -1
- package/lib/locales/de.json +10 -0
- package/lib/locales/en.json +10 -0
- package/lib/locales/es.json +10 -0
- package/lib/locales/fr.json +10 -0
- package/lib/locales/it.json +465 -0
- package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts +12 -2
- package/lib/orbital/ui/AtomicOrbitalCanvas.d.ts.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalCanvas.js +17 -3
- package/lib/orbital/ui/AtomicOrbitalCanvas.js.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalControls.d.ts +10 -5
- package/lib/orbital/ui/AtomicOrbitalControls.d.ts.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalControls.js +12 -7
- package/lib/orbital/ui/AtomicOrbitalControls.js.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalViewer.d.ts +8 -1
- package/lib/orbital/ui/AtomicOrbitalViewer.d.ts.map +1 -1
- package/lib/orbital/ui/AtomicOrbitalViewer.js.map +1 -1
- package/lib/periodic/core/index.d.ts +1 -1
- package/lib/periodic/core/index.d.ts.map +1 -1
- package/lib/periodic/core/index.js +1 -1
- package/lib/periodic/core/index.js.map +1 -1
- package/lib/periodic/core/layout.d.ts +18 -0
- package/lib/periodic/core/layout.d.ts.map +1 -1
- package/lib/periodic/core/layout.js +9 -0
- package/lib/periodic/core/layout.js.map +1 -1
- package/lib/periodic/ui/ElementCell.js +15 -3
- package/lib/periodic/ui/ElementCell.js.map +1 -1
- package/lib/periodic/ui/PeriodicTable.d.ts +12 -1
- package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
- package/lib/periodic/ui/PeriodicTable.js +25 -3
- package/lib/periodic/ui/PeriodicTable.js.map +1 -1
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/axis/core/axisMarks.ts +114 -0
- package/src/axis/core/axisScale.ts +41 -0
- package/src/axis/core/index.ts +20 -0
- package/src/axis/core/logAxisBounds.ts +198 -0
- package/src/axis/ui/AxisScaleControl.tsx +103 -0
- package/src/axis/ui/index.ts +9 -0
- package/src/core.ts +1 -0
- package/src/delimited/core/index.ts +2 -0
- package/src/delimited/core/tableRows.ts +93 -0
- package/src/delimited/ui/DelimitedTextPanel.tsx +25 -9
- package/src/delimited/ui/TableDataButton.tsx +162 -0
- package/src/delimited/ui/index.ts +2 -0
- package/src/i18n/core/chromeCatalog.ts +10 -4
- package/src/i18n/core/index.ts +2 -0
- package/src/i18n/core/languages.ts +5 -4
- package/src/i18n/core/prose.ts +53 -0
- package/src/i18n/ui/Prose.tsx +47 -0
- package/src/i18n/ui/index.ts +2 -0
- package/src/locales/de.json +10 -0
- package/src/locales/en.json +10 -0
- package/src/locales/es.json +10 -0
- package/src/locales/fr.json +10 -0
- package/src/locales/it.json +465 -0
- package/src/orbital/ui/AtomicOrbitalCanvas.tsx +32 -5
- package/src/orbital/ui/AtomicOrbitalControls.tsx +26 -6
- package/src/orbital/ui/AtomicOrbitalViewer.tsx +8 -1
- package/src/periodic/core/index.ts +1 -0
- package/src/periodic/core/layout.ts +19 -0
- package/src/periodic/ui/ElementCell.tsx +15 -3
- package/src/periodic/ui/PeriodicTable.tsx +45 -1
- package/src/ui.ts +1 -0
- package/styles/chrome.css +71 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ends of a logarithmic axis, and the values it is graduated at.
|
|
3
|
+
*
|
|
4
|
+
* Both have to be stated rather than left to the charting library, and for two
|
|
5
|
+
* separate reasons.
|
|
6
|
+
*
|
|
7
|
+
* The ends, because a library that finds them itself may not look at every
|
|
8
|
+
* value. nivo is the measured case: it sorts and takes a true minimum and
|
|
9
|
+
* maximum only for a linear axis, and on a log or symmetric-log one `'auto'`
|
|
10
|
+
* resolves to the first value the data carries and the last — so the domain
|
|
11
|
+
* comes out in plotting order, upside down whenever the first point outweighs
|
|
12
|
+
* the last, and cutting off everything past the last point drawn.
|
|
13
|
+
*
|
|
14
|
+
* The graduations, because d3 rules a symmetric-log axis at evenly *spaced*
|
|
15
|
+
* values while placing them logarithmically, so the top of a wide axis collects
|
|
16
|
+
* every graduation it has into one unreadable line. Ruling it by decade spreads
|
|
17
|
+
* them evenly, which is also how such a quantity is read.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** What {@link logAxisBounds} answers. */
|
|
21
|
+
export interface LogAxisBounds {
|
|
22
|
+
/** The low end of the axis. */
|
|
23
|
+
min: number;
|
|
24
|
+
/** The high end. */
|
|
25
|
+
max: number;
|
|
26
|
+
/**
|
|
27
|
+
* The values to rule it at, or `undefined` for an axis spanning too few
|
|
28
|
+
* decades to be worth ruling by them — there the library's own linear
|
|
29
|
+
* graduations read better, and there is no pile-up to undo.
|
|
30
|
+
*/
|
|
31
|
+
tickValues?: number[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What {@link logAxisBounds} needs beyond the values. */
|
|
35
|
+
export interface LogAxisBoundsOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Width of the linear region at the bottom of a symmetric-log axis — d3's
|
|
38
|
+
* `constant`. Everything smaller than it is placed almost on the zero line,
|
|
39
|
+
* so no graduation is written below it; a graduation there would land on top
|
|
40
|
+
* of the one before it, which is the pile-up this exists to prevent.
|
|
41
|
+
*
|
|
42
|
+
* Pass `0` for a true logarithmic axis, which has no such region — and which
|
|
43
|
+
* cannot place a zero or a negative at all, so those values are then left out
|
|
44
|
+
* of the ends rather than dragging them to nothing.
|
|
45
|
+
* @default 1
|
|
46
|
+
*/
|
|
47
|
+
constant?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Whether the ends are rounded outwards to whole powers of ten. An axis that
|
|
50
|
+
* ends on its own extreme values reads as though those were round numbers;
|
|
51
|
+
* one that ends on decades says what it is. Worth it on a true logarithmic
|
|
52
|
+
* axis, where every graduation is a decade anyway.
|
|
53
|
+
* @default false — the ends are the data's own
|
|
54
|
+
*/
|
|
55
|
+
snapToDecades?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* How many decades the axis may span at most, counted down from its top. A
|
|
58
|
+
* quantity that reaches zero asymptotically has no meaningful bottom, and an
|
|
59
|
+
* axis given one spends most of its height on values nobody is reading.
|
|
60
|
+
* Only consulted with `snapToDecades`.
|
|
61
|
+
* @default Infinity — the axis spans whatever the data does
|
|
62
|
+
*/
|
|
63
|
+
maxDecades?: number;
|
|
64
|
+
/**
|
|
65
|
+
* How many graduations the axis may carry. More than this is a smear however
|
|
66
|
+
* they are spread, so the decades are stepped over rather than written out.
|
|
67
|
+
* @default 10
|
|
68
|
+
*/
|
|
69
|
+
mostTicks?: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Under this many decades the library's own graduations read better. */
|
|
73
|
+
const RULE_PAST_DECADES = 3;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Where a logarithmic axis starts and ends, and where it is graduated.
|
|
77
|
+
* @param values - Every value plotted on the axis. Anything not finite is
|
|
78
|
+
* ignored, so a sheet's gaps need not be filtered out first.
|
|
79
|
+
* @param options - See {@link LogAxisBoundsOptions}.
|
|
80
|
+
* @returns The ends and the graduations, or `null` when no value can be drawn
|
|
81
|
+
* and the caller should leave the axis to its library.
|
|
82
|
+
*/
|
|
83
|
+
export function logAxisBounds(
|
|
84
|
+
values: Iterable<number>,
|
|
85
|
+
options: LogAxisBoundsOptions = {},
|
|
86
|
+
): LogAxisBounds | null {
|
|
87
|
+
const { constant = 1, snapToDecades = false } = options;
|
|
88
|
+
const { maxDecades = Infinity, mostTicks = 10 } = options;
|
|
89
|
+
// A true logarithmic axis has no room for a zero or a negative, so they are
|
|
90
|
+
// not what its bottom end is taken from.
|
|
91
|
+
const positiveOnly = constant === 0;
|
|
92
|
+
|
|
93
|
+
let min = Infinity;
|
|
94
|
+
let max = -Infinity;
|
|
95
|
+
for (const value of values) {
|
|
96
|
+
if (!Number.isFinite(value)) continue;
|
|
97
|
+
if (positiveOnly && value <= 0) continue;
|
|
98
|
+
if (value < min) min = value;
|
|
99
|
+
if (value > max) max = value;
|
|
100
|
+
}
|
|
101
|
+
if (min > max) return null;
|
|
102
|
+
|
|
103
|
+
if (snapToDecades) {
|
|
104
|
+
const top = Math.ceil(Math.log10(Math.abs(max) || 1));
|
|
105
|
+
const bottom = Math.max(
|
|
106
|
+
Math.floor(Math.log10(Math.abs(min) || 1)),
|
|
107
|
+
top - maxDecades,
|
|
108
|
+
);
|
|
109
|
+
return withTicks(decade(bottom), decade(top), constant, mostTicks);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A lone value, or a column of identical ones, spans nothing: reach to the
|
|
113
|
+
// origin so it is read against a zero rather than filling the axis.
|
|
114
|
+
if (min === max) {
|
|
115
|
+
if (min === 0) return { min: -1, max: 1 };
|
|
116
|
+
return { min: Math.min(min, 0), max: Math.max(max, 0) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return withTicks(min, max, constant, mostTicks);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The ends, with the graduations when they are worth writing.
|
|
124
|
+
* @param min
|
|
125
|
+
* @param max
|
|
126
|
+
* @param constant
|
|
127
|
+
* @param mostTicks
|
|
128
|
+
*/
|
|
129
|
+
function withTicks(
|
|
130
|
+
min: number,
|
|
131
|
+
max: number,
|
|
132
|
+
constant: number,
|
|
133
|
+
mostTicks: number,
|
|
134
|
+
): LogAxisBounds {
|
|
135
|
+
const ticks = decadesBetween(min, max, constant, mostTicks);
|
|
136
|
+
return ticks === undefined ? { min, max } : { min, max, tickValues: ticks };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Zero, and the powers of ten the axis both reaches and can separate.
|
|
141
|
+
* @param min - The low end of the axis.
|
|
142
|
+
* @param max - Its high end.
|
|
143
|
+
* @param constant - Width of the linear region; see the options.
|
|
144
|
+
* @param mostTicks - How many graduations the axis may carry.
|
|
145
|
+
* @returns The values to rule it at, or nothing to leave it to the library.
|
|
146
|
+
*/
|
|
147
|
+
function decadesBetween(
|
|
148
|
+
min: number,
|
|
149
|
+
max: number,
|
|
150
|
+
constant: number,
|
|
151
|
+
mostTicks: number,
|
|
152
|
+
): number[] | undefined {
|
|
153
|
+
const largest = Math.max(Math.abs(min), Math.abs(max));
|
|
154
|
+
if (largest === 0) return undefined;
|
|
155
|
+
|
|
156
|
+
// Below the linear region every decade lands on the same pixel, so the axis
|
|
157
|
+
// is ruled from there up rather than from its smallest value.
|
|
158
|
+
const floor = constant > 0 ? Math.log10(constant) : smallestDecade(min, max);
|
|
159
|
+
const bottom = Math.ceil(floor);
|
|
160
|
+
const top = Math.floor(Math.log10(largest));
|
|
161
|
+
// Under a few decades the library's own graduations read better — but only
|
|
162
|
+
// where it has linear ones to offer. On a true logarithmic axis every
|
|
163
|
+
// graduation is a decade, so a short one is still ruled by decade.
|
|
164
|
+
if (constant > 0 && top - bottom + 1 < RULE_PAST_DECADES) return undefined;
|
|
165
|
+
|
|
166
|
+
const step = Math.ceil((top - bottom + 1) / mostTicks);
|
|
167
|
+
const ticks = min <= 0 && max >= 0 ? [0] : [];
|
|
168
|
+
for (let power = bottom; power <= top; power += step) {
|
|
169
|
+
const tick = decade(power);
|
|
170
|
+
if (tick >= min && tick <= max) ticks.push(tick);
|
|
171
|
+
}
|
|
172
|
+
if (constant === 0) return ticks;
|
|
173
|
+
return ticks.length < RULE_PAST_DECADES ? undefined : ticks;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A whole power of ten.
|
|
178
|
+
* @param power - The exponent, a whole number.
|
|
179
|
+
* @returns Ten to that power, the same bits on every engine.
|
|
180
|
+
*/
|
|
181
|
+
function decade(power: number): number {
|
|
182
|
+
// Read as a decimal rather than computed: `10 ** power` goes through pow,
|
|
183
|
+
// which is not bit-exact across V8 versions — node 22 answers
|
|
184
|
+
// 0.00009999999999999999 for -4 and 1.0000000000000001e-20 for -20 — while a
|
|
185
|
+
// decimal literal is the correctly-rounded double everywhere.
|
|
186
|
+
return Number(`1e${power}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The decade of the smaller end, for an axis with no linear region.
|
|
191
|
+
* @param min - The low end of the axis.
|
|
192
|
+
* @param max - Its high end.
|
|
193
|
+
* @returns The base-ten logarithm of the smaller end that is not zero.
|
|
194
|
+
*/
|
|
195
|
+
function smallestDecade(min: number, max: number): number {
|
|
196
|
+
const ends = [Math.abs(min), Math.abs(max)].filter((end) => end > 0);
|
|
197
|
+
return Math.log10(Math.min(...ends));
|
|
198
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control that moves one axis between its two scales.
|
|
3
|
+
*
|
|
4
|
+
* It belongs on the figure rather than in the page around it: the scale is not
|
|
5
|
+
* a property of the page but of the picture, a reader decides to change it
|
|
6
|
+
* while looking at the picture, and a figure handed out on its own — embedded
|
|
7
|
+
* in a course page, pasted into a deck — takes its controls with it. So it is
|
|
8
|
+
* written as a control of an `OverlayBar`, floating in whichever corner the
|
|
9
|
+
* marks leave emptiest.
|
|
10
|
+
*
|
|
11
|
+
* Its two segments are named here rather than by each site, because `Linear`
|
|
12
|
+
* and `Log` are chrome: one translation reaches every figure in the family.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ReactElement } from 'react';
|
|
16
|
+
|
|
17
|
+
import type { HelpContent } from '../../help/ui/HelpBody.tsx';
|
|
18
|
+
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
19
|
+
import type { OverlayOption } from '../../overlay/ui/OverlayRow.tsx';
|
|
20
|
+
import { OverlaySegmented } from '../../overlay/ui/OverlaySegmented.tsx';
|
|
21
|
+
import type { AxisScale } from '../core/axisScale.ts';
|
|
22
|
+
|
|
23
|
+
/** Which axis a control belongs to, which is how it names itself. */
|
|
24
|
+
export type AxisScaleAxis = 'x' | 'y';
|
|
25
|
+
|
|
26
|
+
/** What {@link AxisScaleControl} needs. */
|
|
27
|
+
export interface AxisScaleControlProps {
|
|
28
|
+
/** The scale in force — what `resolveAxisScale` answered, not the choice. */
|
|
29
|
+
value: AxisScale;
|
|
30
|
+
/** Called with the scale the reader picked. */
|
|
31
|
+
onChange: (scale: AxisScale) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Which axis it sets, which names it `X scale` or `Y scale`. Leave it out on
|
|
34
|
+
* a figure where only one axis offers the choice: the control is then simply
|
|
35
|
+
* `Scale`, because naming the axis is only worth the width when there are two
|
|
36
|
+
* of them to tell apart.
|
|
37
|
+
* @default undefined — the control is named `Scale`
|
|
38
|
+
*/
|
|
39
|
+
axis?: AxisScaleAxis;
|
|
40
|
+
/**
|
|
41
|
+
* What it is called, overriding both of the above. For a figure whose axis
|
|
42
|
+
* has a name the reader already knows it by.
|
|
43
|
+
* @default the chrome's own word, in the language of the page
|
|
44
|
+
*/
|
|
45
|
+
label?: string;
|
|
46
|
+
/**
|
|
47
|
+
* What the two scales do, in a sentence.
|
|
48
|
+
* @default the chrome's own explanation
|
|
49
|
+
*/
|
|
50
|
+
help?: HelpContent;
|
|
51
|
+
/**
|
|
52
|
+
* Whether the name is announced but not written.
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
hideLabel?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether the control is greyed and unreachable.
|
|
58
|
+
* @default false
|
|
59
|
+
*/
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Value of the `data-testid` attribute.
|
|
63
|
+
* @default undefined
|
|
64
|
+
*/
|
|
65
|
+
testId?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The two scales, the one in force pressed.
|
|
70
|
+
* @param props - See {@link AxisScaleControlProps}.
|
|
71
|
+
* @returns The named pair of segments.
|
|
72
|
+
*/
|
|
73
|
+
export function AxisScaleControl(props: AxisScaleControlProps): ReactElement {
|
|
74
|
+
const { value, onChange, axis, label, help, testId } = props;
|
|
75
|
+
const { hideLabel = false, disabled = false } = props;
|
|
76
|
+
const t = useChromeT();
|
|
77
|
+
|
|
78
|
+
const options: ReadonlyArray<OverlayOption<AxisScale>> = [
|
|
79
|
+
{ value: 'linear', label: t('axis.linear') },
|
|
80
|
+
{ value: 'log', label: t('axis.logarithmic') },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<OverlaySegmented<AxisScale>
|
|
85
|
+
label={label ?? t(nameOf(axis))}
|
|
86
|
+
help={help ?? { title: t('axis.scale'), body: t('axis.scaleHelp') }}
|
|
87
|
+
hideLabel={hideLabel}
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
value={value}
|
|
90
|
+
options={options}
|
|
91
|
+
onChange={onChange}
|
|
92
|
+
testId={testId}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function nameOf(
|
|
98
|
+
axis: AxisScaleAxis | undefined,
|
|
99
|
+
): 'axis.scale' | 'axis.xScale' | 'axis.yScale' {
|
|
100
|
+
if (axis === 'x') return 'axis.xScale';
|
|
101
|
+
if (axis === 'y') return 'axis.yScale';
|
|
102
|
+
return 'axis.scale';
|
|
103
|
+
}
|
package/src/core.ts
CHANGED
|
@@ -8,3 +8,5 @@ export type { ReadDelimitedOptions } from './readDelimited.ts';
|
|
|
8
8
|
export { detectDelimiter, readDelimited } from './readDelimited.ts';
|
|
9
9
|
export type { ToDelimitedOptions } from './toDelimited.ts';
|
|
10
10
|
export { escapeCell, toDelimited } from './toDelimited.ts';
|
|
11
|
+
export type { TableRecord, TableRows, TableRowsOptions } from './tableRows.ts';
|
|
12
|
+
export { columnsOf, tableRows } from './tableRows.ts';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a list of records into the cells a table is handed over as.
|
|
3
|
+
*
|
|
4
|
+
* Most pages hold their data as one object per row — a query result, a list of
|
|
5
|
+
* peaks, a row of properties — while a delimited table is cells. Every site
|
|
6
|
+
* that exports such a list wrote the same two loops, so they are here once.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** One record, as a page holds it before it becomes a row of cells. */
|
|
10
|
+
export type TableRecord = Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
/** How a record becomes cells. */
|
|
13
|
+
export interface TableRowsOptions {
|
|
14
|
+
/**
|
|
15
|
+
* The columns to write, in order. A column no record carries comes out
|
|
16
|
+
* empty, which is what lets one caller pin the order of a table whose first
|
|
17
|
+
* record happens to be missing a field.
|
|
18
|
+
* @default the keys of the first record, in the order it declares them
|
|
19
|
+
*/
|
|
20
|
+
columns?: readonly string[];
|
|
21
|
+
/**
|
|
22
|
+
* How a value becomes a cell.
|
|
23
|
+
* @default `String(value)`, with `null` and `undefined` written as nothing
|
|
24
|
+
*/
|
|
25
|
+
format?: (value: unknown, column: string) => string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The columns, and the cells under them. */
|
|
29
|
+
export interface TableRows {
|
|
30
|
+
/** Column names, in the order the cells are written. */
|
|
31
|
+
header: string[];
|
|
32
|
+
/** One array of cells per record. */
|
|
33
|
+
rows: string[][];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The records as the header and cells of a table.
|
|
38
|
+
* @param records - The records, in the order they are shown.
|
|
39
|
+
* @param options - See {@link TableRowsOptions}.
|
|
40
|
+
* @returns See {@link TableRows}.
|
|
41
|
+
*/
|
|
42
|
+
export function tableRows(
|
|
43
|
+
records: readonly TableRecord[],
|
|
44
|
+
options: TableRowsOptions = {},
|
|
45
|
+
): TableRows {
|
|
46
|
+
const { columns, format = defaultCell } = options;
|
|
47
|
+
const header = [...(columns ?? columnsOf(records))];
|
|
48
|
+
const rows: string[][] = [];
|
|
49
|
+
for (const record of records) {
|
|
50
|
+
const cells: string[] = [];
|
|
51
|
+
for (const column of header) cells.push(format(record[column], column));
|
|
52
|
+
rows.push(cells);
|
|
53
|
+
}
|
|
54
|
+
return { header, rows };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Every column the records carry, each named once, in the order first seen.
|
|
59
|
+
*
|
|
60
|
+
* Every record is read rather than only the first, because a list whose later
|
|
61
|
+
* records carry a field the first one happens to lack would otherwise be
|
|
62
|
+
* handed over with that column silently missing.
|
|
63
|
+
* @param records - The records.
|
|
64
|
+
* @returns The column names.
|
|
65
|
+
*/
|
|
66
|
+
export function columnsOf(records: readonly TableRecord[]): string[] {
|
|
67
|
+
const columns: string[] = [];
|
|
68
|
+
const seen = new Set<string>();
|
|
69
|
+
for (const record of records) {
|
|
70
|
+
for (const column of Object.keys(record)) {
|
|
71
|
+
if (seen.has(column)) continue;
|
|
72
|
+
seen.add(column);
|
|
73
|
+
columns.push(column);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return columns;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function defaultCell(value: unknown): string {
|
|
80
|
+
if (value === null || value === undefined) return '';
|
|
81
|
+
if (typeof value === 'string') return value;
|
|
82
|
+
if (
|
|
83
|
+
typeof value === 'number' ||
|
|
84
|
+
typeof value === 'bigint' ||
|
|
85
|
+
typeof value === 'boolean'
|
|
86
|
+
) {
|
|
87
|
+
return String(value);
|
|
88
|
+
}
|
|
89
|
+
// Anything else left in a cell has no honest text form, and `[object Object]`
|
|
90
|
+
// is the one answer a spreadsheet can do nothing with. JSON at least says
|
|
91
|
+
// what was there; a caller who wants better passes `format`.
|
|
92
|
+
return JSON.stringify(value) ?? '';
|
|
93
|
+
}
|
|
@@ -54,6 +54,15 @@ export interface DelimitedTextPanelProps {
|
|
|
54
54
|
* @default 320
|
|
55
55
|
*/
|
|
56
56
|
height?: number;
|
|
57
|
+
/**
|
|
58
|
+
* What is shown in place of the text area. A caller with more rows than a
|
|
59
|
+
* text area can hold — a spectrum of a hundred thousand points — passes its
|
|
60
|
+
* own virtualized table here; what is copied and what is saved stay the
|
|
61
|
+
* serialized text either way, so the preview can shorten a number the file
|
|
62
|
+
* keeps in full.
|
|
63
|
+
* @default undefined — the text itself, in a read-only text area
|
|
64
|
+
*/
|
|
65
|
+
preview?: ReactNode;
|
|
57
66
|
/**
|
|
58
67
|
* Class names added to the root element, after the component's own.
|
|
59
68
|
* @default undefined
|
|
@@ -84,6 +93,7 @@ export function DelimitedTextPanel(
|
|
|
84
93
|
downloadable = true,
|
|
85
94
|
label,
|
|
86
95
|
height = 320,
|
|
96
|
+
preview,
|
|
87
97
|
} = props;
|
|
88
98
|
const t = useChromeT();
|
|
89
99
|
const [delimiterId, setDelimiterId] = useState<string>(defaultDelimiter);
|
|
@@ -138,15 +148,19 @@ export function DelimitedTextPanel(
|
|
|
138
148
|
small
|
|
139
149
|
/>
|
|
140
150
|
</div>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
{preview === undefined ? (
|
|
152
|
+
<textarea
|
|
153
|
+
readOnly
|
|
154
|
+
value={text}
|
|
155
|
+
spellCheck={false}
|
|
156
|
+
aria-label={label ?? t('delimited.tableAsText')}
|
|
157
|
+
className={Classes.INPUT}
|
|
158
|
+
style={{ ...TEXT_STYLE, height }}
|
|
159
|
+
onFocus={(event) => event.currentTarget.select()}
|
|
160
|
+
/>
|
|
161
|
+
) : (
|
|
162
|
+
<div style={PREVIEW_STYLE}>{preview}</div>
|
|
163
|
+
)}
|
|
150
164
|
</div>
|
|
151
165
|
);
|
|
152
166
|
}
|
|
@@ -166,6 +180,8 @@ const LABEL_STYLE = {
|
|
|
166
180
|
|
|
167
181
|
const SPACER_STYLE = { flex: 1 } as const satisfies CSSProperties;
|
|
168
182
|
|
|
183
|
+
const PREVIEW_STYLE = { marginTop: 12 } as const satisfies CSSProperties;
|
|
184
|
+
|
|
169
185
|
const TEXT_STYLE = {
|
|
170
186
|
width: '100%',
|
|
171
187
|
marginTop: 12,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { IconName } from '@blueprintjs/core';
|
|
2
|
+
import { Button } from '@blueprintjs/core';
|
|
3
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
7
|
+
|
|
8
|
+
import type { DelimitedTextDialogProps } from './DelimitedTextDialog.tsx';
|
|
9
|
+
import { DelimitedTextDialog } from './DelimitedTextDialog.tsx';
|
|
10
|
+
|
|
11
|
+
/** The cells of a table, one array per line. */
|
|
12
|
+
type TableRows = ReadonlyArray<readonly string[]>;
|
|
13
|
+
|
|
14
|
+
/** What the shut dialog is handed, so it holds no copy of the last table. */
|
|
15
|
+
const EMPTY: TableRows = [];
|
|
16
|
+
|
|
17
|
+
/** What {@link TableDataButton} hands over, and how the button reads. */
|
|
18
|
+
export interface TableDataButtonProps extends Omit<
|
|
19
|
+
DelimitedTextDialogProps,
|
|
20
|
+
'isOpen' | 'onClose' | 'rows'
|
|
21
|
+
> {
|
|
22
|
+
/**
|
|
23
|
+
* The cells, one array per line. A function is called when the dialog opens
|
|
24
|
+
* and not before, which is what a table built out of a computation has to
|
|
25
|
+
* be: formatting a thousand points into strings on every render, for a
|
|
26
|
+
* button nobody may press, is a page that stutters while it is used.
|
|
27
|
+
*/
|
|
28
|
+
rows: TableRows | (() => TableRows);
|
|
29
|
+
/**
|
|
30
|
+
* Text of the button. Left out for an icon-only button, which is what a
|
|
31
|
+
* dense toolbar over a table wants.
|
|
32
|
+
* @default undefined — the button is reduced to its icon
|
|
33
|
+
*/
|
|
34
|
+
text?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Glyph on the button.
|
|
37
|
+
* @default 'th'
|
|
38
|
+
*/
|
|
39
|
+
icon?: IconName;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the button drops its background, for a toolbar or a card header.
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
minimal?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether the button is the small size.
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
small?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* What the pointer and a screen reader are told. The dialog's own `title`
|
|
52
|
+
* names the table — `Titration curve as a table` — while this names the
|
|
53
|
+
* affordance, and the two are deliberately not the same string: the heading
|
|
54
|
+
* is the site's to write, the button's name is the family's, so the control
|
|
55
|
+
* reads the same on every tool a visitor opens that week.
|
|
56
|
+
* @default the chrome's own line, in the language of the page
|
|
57
|
+
*/
|
|
58
|
+
tooltip?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Whether there is nothing to hand over. A table that has not been computed
|
|
61
|
+
* yet says so by being greyed rather than by opening an empty dialog.
|
|
62
|
+
* @default whether there are no rows — and `false` where `rows` is a
|
|
63
|
+
* function, which is not called to find out
|
|
64
|
+
*/
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Class the button carries, so a site can reach it from its stylesheet — a
|
|
68
|
+
* terminal whose green the chrome's muted grey is unreadable against, a
|
|
69
|
+
* toolbar with its own metrics.
|
|
70
|
+
* @default undefined
|
|
71
|
+
*/
|
|
72
|
+
buttonClassName?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Value of the `data-testid` attribute of the button.
|
|
75
|
+
* @default undefined
|
|
76
|
+
*/
|
|
77
|
+
testId?: string;
|
|
78
|
+
/**
|
|
79
|
+
* The control in place of the Blueprint button, for a page whose chrome is
|
|
80
|
+
* not Blueprint's — a `react-science` toolbar, a site's own toolbar button.
|
|
81
|
+
* It is handed the function that opens the dialog.
|
|
82
|
+
*
|
|
83
|
+
* It exists so the dialog, the separator, the copy, the save and the wording
|
|
84
|
+
* are shared even where the button cannot be: a site that keeps its own
|
|
85
|
+
* trigger should not also be keeping its own dialog.
|
|
86
|
+
* @default a Blueprint button carrying the glyph and the text
|
|
87
|
+
*/
|
|
88
|
+
trigger?: (open: () => void) => ReactNode;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The one control that takes a table off the page: copy it, or save it.
|
|
93
|
+
*
|
|
94
|
+
* It is a button and a dialog together rather than a dialog a site has to hold
|
|
95
|
+
* open itself, because the state behind it is the same three lines in every
|
|
96
|
+
* site and because the wording is not a site's to choose. An audit in
|
|
97
|
+
* September 2026 found the same affordance spelled `Export as TSV` on one
|
|
98
|
+
* site, `Copy as TSV` on another and `Copy or download data` on a third, which
|
|
99
|
+
* reads as three different features to anybody who uses two of our tools in a
|
|
100
|
+
* week.
|
|
101
|
+
*
|
|
102
|
+
* It is also deliberately not named after a separator. The dialog behind it
|
|
103
|
+
* writes tab-, comma- or semicolon-separated text, so a button that says TSV
|
|
104
|
+
* is wrong two times out of three — and a reader looking for CSV concludes the
|
|
105
|
+
* tool cannot give it to them.
|
|
106
|
+
* @param props - See {@link TableDataButtonProps}.
|
|
107
|
+
* @returns The button and its dialog.
|
|
108
|
+
*/
|
|
109
|
+
export function TableDataButton(props: TableDataButtonProps): ReactElement {
|
|
110
|
+
const {
|
|
111
|
+
rows,
|
|
112
|
+
text,
|
|
113
|
+
icon = 'th',
|
|
114
|
+
minimal = true,
|
|
115
|
+
small = false,
|
|
116
|
+
tooltip,
|
|
117
|
+
disabled,
|
|
118
|
+
testId,
|
|
119
|
+
trigger,
|
|
120
|
+
buttonClassName,
|
|
121
|
+
...dialog
|
|
122
|
+
} = props;
|
|
123
|
+
const t = useChromeT();
|
|
124
|
+
// The cells are held rather than read on every render, so a `rows` function
|
|
125
|
+
// is called once per opening and the closed button costs nothing at all.
|
|
126
|
+
const [shown, setShown] = useState<TableRows | null>(null);
|
|
127
|
+
|
|
128
|
+
const told = tooltip ?? t('delimited.copyOrDownload');
|
|
129
|
+
const nothing =
|
|
130
|
+
disabled ?? (typeof rows === 'function' ? false : rows.length === 0);
|
|
131
|
+
|
|
132
|
+
function show(): void {
|
|
133
|
+
setShown(typeof rows === 'function' ? rows() : rows);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<>
|
|
138
|
+
{trigger ? (
|
|
139
|
+
trigger(show)
|
|
140
|
+
) : (
|
|
141
|
+
<Button
|
|
142
|
+
className={buttonClassName}
|
|
143
|
+
icon={icon}
|
|
144
|
+
text={text}
|
|
145
|
+
variant={minimal ? 'minimal' : 'solid'}
|
|
146
|
+
size={small ? 'small' : 'medium'}
|
|
147
|
+
disabled={nothing}
|
|
148
|
+
title={told}
|
|
149
|
+
aria-label={text ?? told}
|
|
150
|
+
data-testid={testId}
|
|
151
|
+
onClick={show}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
<DelimitedTextDialog
|
|
155
|
+
{...dialog}
|
|
156
|
+
rows={shown ?? EMPTY}
|
|
157
|
+
isOpen={shown !== null}
|
|
158
|
+
onClose={() => setShown(null)}
|
|
159
|
+
/>
|
|
160
|
+
</>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -2,3 +2,5 @@ export type { DelimitedTextDialogProps } from './DelimitedTextDialog.tsx';
|
|
|
2
2
|
export { DelimitedTextDialog } from './DelimitedTextDialog.tsx';
|
|
3
3
|
export type { DelimitedTextPanelProps } from './DelimitedTextPanel.tsx';
|
|
4
4
|
export { DelimitedTextPanel } from './DelimitedTextPanel.tsx';
|
|
5
|
+
export type { TableDataButtonProps } from './TableDataButton.tsx';
|
|
6
|
+
export { TableDataButton } from './TableDataButton.tsx';
|
|
@@ -19,7 +19,7 @@ export type ChromeKey = keyof typeof en;
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* The chrome's messages. English is part of the page because it is what a
|
|
22
|
-
* missing message falls back to;
|
|
22
|
+
* missing message falls back to; every other language arrives as a chunk of its
|
|
23
23
|
* own, so a site read only in English downloads none of them.
|
|
24
24
|
*/
|
|
25
25
|
export const CHROME_CATALOG = new MessageCatalog<ChromeKey>({
|
|
@@ -27,9 +27,15 @@ export const CHROME_CATALOG = new MessageCatalog<ChromeKey>({
|
|
|
27
27
|
repository: 'cheminfo/react-cheminfo',
|
|
28
28
|
directory: 'src/locales',
|
|
29
29
|
source: en,
|
|
30
|
+
// No `with { type: 'json' }` on these: Vite turns a JSON file into a real
|
|
31
|
+
// JavaScript module, and the attribute makes the browser refuse the chunk it
|
|
32
|
+
// serves for being `text/javascript` — the chrome then stays in English with
|
|
33
|
+
// nothing failing at build or type-check time. The English import above is
|
|
34
|
+
// static, so the bundler resolves it before a browser ever sees it.
|
|
30
35
|
translations: {
|
|
31
|
-
fr: () => import('../../locales/fr.json'
|
|
32
|
-
de: () => import('../../locales/de.json'
|
|
33
|
-
es: () => import('../../locales/es.json'
|
|
36
|
+
fr: () => import('../../locales/fr.json'),
|
|
37
|
+
de: () => import('../../locales/de.json'),
|
|
38
|
+
es: () => import('../../locales/es.json'),
|
|
39
|
+
it: () => import('../../locales/it.json'),
|
|
34
40
|
},
|
|
35
41
|
});
|
package/src/i18n/core/index.ts
CHANGED
|
@@ -15,6 +15,8 @@ export type {
|
|
|
15
15
|
} from './messageCatalog.ts';
|
|
16
16
|
export { MessageCatalog } from './messageCatalog.ts';
|
|
17
17
|
export type { MessageValues } from './messageValues.ts';
|
|
18
|
+
export type { ProsePiece } from './prose.ts';
|
|
19
|
+
export { splitProse } from './prose.ts';
|
|
18
20
|
export type { MessageSession } from './session.ts';
|
|
19
21
|
export {
|
|
20
22
|
messageSession,
|