react-cheminfo 0.45.0 → 0.47.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/lib/chart/core/chartDomain.d.ts +30 -5
- package/lib/chart/core/chartDomain.d.ts.map +1 -1
- package/lib/chart/core/chartDomain.js +1 -0
- package/lib/chart/core/chartDomain.js.map +1 -1
- package/lib/chart/core/index.d.ts +1 -1
- package/lib/chart/core/index.d.ts.map +1 -1
- package/lib/chart/core/index.js +1 -1
- package/lib/chart/core/index.js.map +1 -1
- package/lib/chart/core/selectionGeometry.d.ts +8 -5
- package/lib/chart/core/selectionGeometry.d.ts.map +1 -1
- package/lib/chart/core/selectionGeometry.js +8 -5
- package/lib/chart/core/selectionGeometry.js.map +1 -1
- package/lib/chart/core/zoomDomain.d.ts +39 -4
- package/lib/chart/core/zoomDomain.d.ts.map +1 -1
- package/lib/chart/core/zoomDomain.js +90 -22
- package/lib/chart/core/zoomDomain.js.map +1 -1
- package/lib/chart/ui/SelectionRect.d.ts.map +1 -1
- package/lib/chart/ui/SelectionRect.js +1 -1
- package/lib/chart/ui/SelectionRect.js.map +1 -1
- package/lib/chart/ui/chartGestures.d.ts +26 -1
- package/lib/chart/ui/chartGestures.d.ts.map +1 -1
- package/lib/chart/ui/chartGestures.js +21 -0
- package/lib/chart/ui/chartGestures.js.map +1 -1
- package/lib/chart/ui/index.d.ts +1 -1
- package/lib/chart/ui/index.d.ts.map +1 -1
- package/lib/chart/ui/index.js +1 -1
- package/lib/chart/ui/index.js.map +1 -1
- package/lib/chart/ui/useChartZoom.d.ts.map +1 -1
- package/lib/chart/ui/useChartZoom.js +17 -1
- package/lib/chart/ui/useChartZoom.js.map +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts +1 -1
- package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
- package/lib/ir/core/irMode.d.ts.map +1 -1
- package/lib/ir/core/irMode.js +6 -1
- package/lib/ir/core/irMode.js.map +1 -1
- package/lib/locales/de.json +1 -0
- package/lib/locales/en.json +1 -0
- package/lib/locales/es.json +1 -0
- package/lib/locales/fr.json +1 -0
- package/lib/locales/it.json +1 -0
- package/lib/periodic/core/index.d.ts +1 -1
- package/lib/periodic/core/index.d.ts.map +1 -1
- package/lib/periodic/core/layout.d.ts +11 -0
- package/lib/periodic/core/layout.d.ts.map +1 -1
- package/lib/periodic/core/layout.js.map +1 -1
- package/lib/periodic/ui/ElementCell.d.ts +6 -2
- package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
- package/lib/periodic/ui/ElementCell.js +2 -2
- package/lib/periodic/ui/ElementCell.js.map +1 -1
- package/lib/periodic/ui/PeriodicTable.d.ts +12 -3
- package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
- package/lib/periodic/ui/PeriodicTable.js +5 -3
- package/lib/periodic/ui/PeriodicTable.js.map +1 -1
- package/lib/periodic/ui/PeriodicTableChrome.d.ts +13 -3
- package/lib/periodic/ui/PeriodicTableChrome.d.ts.map +1 -1
- package/lib/periodic/ui/PeriodicTableChrome.js +18 -6
- package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -1
- package/lib/split/ui/SplitColumn.d.ts +30 -0
- package/lib/split/ui/SplitColumn.d.ts.map +1 -0
- package/lib/split/ui/SplitColumn.js +20 -0
- package/lib/split/ui/SplitColumn.js.map +1 -0
- package/lib/split/ui/SplitRow.d.ts +15 -53
- package/lib/split/ui/SplitRow.d.ts.map +1 -1
- package/lib/split/ui/SplitRow.js +9 -90
- package/lib/split/ui/SplitRow.js.map +1 -1
- package/lib/split/ui/index.d.ts +3 -0
- package/lib/split/ui/index.d.ts.map +1 -1
- package/lib/split/ui/index.js +1 -0
- package/lib/split/ui/index.js.map +1 -1
- package/lib/split/ui/splitPanes.d.ts +73 -0
- package/lib/split/ui/splitPanes.d.ts.map +1 -0
- package/lib/split/ui/splitPanes.js +128 -0
- package/lib/split/ui/splitPanes.js.map +1 -0
- package/package.json +1 -1
- package/src/chart/core/chartDomain.ts +31 -5
- package/src/chart/core/index.ts +2 -0
- package/src/chart/core/selectionGeometry.ts +8 -5
- package/src/chart/core/zoomDomain.ts +109 -26
- package/src/chart/ui/SelectionRect.tsx +4 -0
- package/src/chart/ui/chartGestures.ts +41 -1
- package/src/chart/ui/index.ts +1 -1
- package/src/chart/ui/useChartZoom.ts +25 -1
- package/src/ir/core/irMode.ts +6 -1
- package/src/locales/de.json +1 -0
- package/src/locales/en.json +1 -0
- package/src/locales/es.json +1 -0
- package/src/locales/fr.json +1 -0
- package/src/locales/it.json +1 -0
- package/src/periodic/core/index.ts +1 -1
- package/src/periodic/core/layout.ts +12 -0
- package/src/periodic/ui/ElementCell.tsx +8 -4
- package/src/periodic/ui/PeriodicTable.tsx +23 -5
- package/src/periodic/ui/PeriodicTableChrome.tsx +28 -4
- package/src/split/ui/SplitColumn.tsx +36 -0
- package/src/split/ui/SplitRow.tsx +17 -208
- package/src/split/ui/index.ts +3 -0
- package/src/split/ui/splitPanes.tsx +267 -0
|
@@ -80,18 +80,29 @@ export const DEFAULT_Y_AXIS_RULES: Required<YAxisRules> = {
|
|
|
80
80
|
* between forty and sixty percent", so a drag narrows the horizontal axis and
|
|
81
81
|
* leaves the height alone unless it is released past the baseline.
|
|
82
82
|
*
|
|
83
|
+
* `dual` is the same reading gesture with a gate a reader can predict: a drag
|
|
84
|
+
* kept level narrows the horizontal axis alone, and one taken far enough up or
|
|
85
|
+
* down brings the height with it. It is what `xAxis` cannot be on a chart whose
|
|
86
|
+
* vertical axis has no baseline to cross — a titration curve climbing from pH 2
|
|
87
|
+
* to pH 12, a speciation diagram drawn over decades — where "has the pointer
|
|
88
|
+
* passed zero" is a question about the margin rather than about the data, and
|
|
89
|
+
* the answer to it is no for every drag made inside the plot. Here both windows
|
|
90
|
+
* are asked for where the curve is, and the preview changes shape as the
|
|
91
|
+
* threshold is crossed, so the rectangle is found by the hand that was already
|
|
92
|
+
* drawing one.
|
|
93
|
+
*
|
|
83
94
|
* `box` is the tool: while it is chosen, the rectangle dragged out is the window,
|
|
84
95
|
* both axes at once. It is a mode rather than a modifier because it is used in
|
|
85
96
|
* runs — a baseline being examined is examined several times over — and a
|
|
86
97
|
* modifier held down through each of them is a worse gesture than a tool pressed
|
|
87
98
|
* once.
|
|
88
99
|
*
|
|
89
|
-
* The
|
|
90
|
-
* nothing but zoom tools can say so in its own signature — which is
|
|
91
|
-
* `select`, the one drag that moves no axis, from reaching a chart
|
|
92
|
-
* handler for it.
|
|
100
|
+
* The three are named apart from the whole of `DragMode` so that a chart
|
|
101
|
+
* offering nothing but zoom tools can say so in its own signature — which is
|
|
102
|
+
* what stops `select`, the one drag that moves no axis, from reaching a chart
|
|
103
|
+
* with no handler for it.
|
|
93
104
|
*/
|
|
94
|
-
export type ZoomDragMode = 'xAxis' | 'box';
|
|
105
|
+
export type ZoomDragMode = 'xAxis' | 'dual' | 'box';
|
|
95
106
|
|
|
96
107
|
/**
|
|
97
108
|
* What a drag across the plot asks for.
|
|
@@ -152,12 +163,27 @@ export interface ZoomGestures {
|
|
|
152
163
|
* @default true
|
|
153
164
|
*/
|
|
154
165
|
wheel?: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Whether the chart answers `f` with a window fitted to everything drawn.
|
|
168
|
+
*
|
|
169
|
+
* The same thing the double click does, reachable without a pointer — and the
|
|
170
|
+
* one gesture a reader cannot discover by trying, which is why the key is the
|
|
171
|
+
* same `f` on every chart of the family rather than each one choosing.
|
|
172
|
+
*
|
|
173
|
+
* Turned off by a viewer that already owns its keys: the infrared shell runs
|
|
174
|
+
* `f` through its own command table, where the key is listed in a toolbar
|
|
175
|
+
* tooltip and in the documentation panel, and fits every pane of a stack
|
|
176
|
+
* rather than the one chart the pointer is on.
|
|
177
|
+
* @default true
|
|
178
|
+
*/
|
|
179
|
+
keys?: boolean;
|
|
155
180
|
}
|
|
156
181
|
|
|
157
182
|
/** What the gestures come to when a viewer states none of them. */
|
|
158
183
|
export const DEFAULT_ZOOM_GESTURES: Required<ZoomGestures> = {
|
|
159
184
|
drag: 'xAxis',
|
|
160
185
|
wheel: true,
|
|
186
|
+
keys: true,
|
|
161
187
|
};
|
|
162
188
|
|
|
163
189
|
/**
|
package/src/chart/core/index.ts
CHANGED
|
@@ -169,7 +169,9 @@ export {
|
|
|
169
169
|
} from './viewTransform.ts';
|
|
170
170
|
export type { DragBox, ZoomSelection } from './zoomDomain.ts';
|
|
171
171
|
export {
|
|
172
|
+
DUAL_ZOOM_TRAVEL,
|
|
172
173
|
MINIMUM_DRAG,
|
|
174
|
+
draggedBeyondLevel,
|
|
173
175
|
releasedBeyondBaseline,
|
|
174
176
|
scaledYAxis,
|
|
175
177
|
zoomSelection,
|
|
@@ -31,12 +31,15 @@ export interface SelectionRectangle {
|
|
|
31
31
|
/**
|
|
32
32
|
* The rectangle to draw for a drag being made.
|
|
33
33
|
*
|
|
34
|
-
* A drag that takes the y axis with it is drawn at the height
|
|
35
|
-
*
|
|
34
|
+
* A drag that takes the y axis with it is drawn at the height `zoomSelection`
|
|
35
|
+
* worked out — the window the release will take, which is the drag's own height
|
|
36
|
+
* stretched to any baseline the viewer holds — and one that asks for the x axis
|
|
37
|
+
* alone spans the whole plot.
|
|
36
38
|
* That is the gesture announcing itself before the button is let go: the
|
|
37
|
-
* rectangle changing shape as the pointer crosses the baseline
|
|
38
|
-
*
|
|
39
|
-
* they can still act
|
|
39
|
+
* rectangle changing shape — as the pointer crosses the baseline, or as a
|
|
40
|
+
* `dual` drag leaves the level it started on — is the only warning a user gets
|
|
41
|
+
* that the release is about to move both axes, and it is one they can still act
|
|
42
|
+
* on.
|
|
40
43
|
*
|
|
41
44
|
* A drag too short to zoom draws nothing at all. Below `MINIMUM_DRAG` the
|
|
42
45
|
* release does nothing — most clicks made on a trackpad wobble by a pixel or two
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { clamp } from '../../format/core/clamp.ts';
|
|
12
12
|
|
|
13
|
-
import type { ChartDomain, ZoomRules } from './chartDomain.ts';
|
|
13
|
+
import type { ChartDomain, DragMode, ZoomRules } from './chartDomain.ts';
|
|
14
14
|
import { DEFAULT_Y_AXIS_RULES } from './chartDomain.ts';
|
|
15
15
|
import type { PlotRect } from './chartGeometry.ts';
|
|
16
16
|
import type { ChartScale } from './chartScale.ts';
|
|
@@ -30,6 +30,19 @@ import { chartPixel, chartValue } from './chartScale.ts';
|
|
|
30
30
|
*/
|
|
31
31
|
export const MINIMUM_DRAG = 4;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* How far a `dual` drag has to travel vertically before it brings the y axis
|
|
35
|
+
* with it, in user units of the SVG.
|
|
36
|
+
*
|
|
37
|
+
* Far enough above `MINIMUM_DRAG` that no drag meant to stay level reaches it by
|
|
38
|
+
* the tremor of a hand or the slope of a trackpad swipe, and near enough that
|
|
39
|
+
* the rectangle a reader wants costs one gesture rather than a reach for the
|
|
40
|
+
* margin. The preview changes shape on the pixel it is crossed, so the number
|
|
41
|
+
* itself never has to be learnt: the first drag that crosses it teaches the
|
|
42
|
+
* gesture, and the first that recrosses it back shows the way out.
|
|
43
|
+
*/
|
|
44
|
+
export const DUAL_ZOOM_TRAVEL = 24;
|
|
45
|
+
|
|
33
46
|
/** The two corners of a drag, in the user units of the SVG. */
|
|
34
47
|
export interface DragBox {
|
|
35
48
|
/** Where the press landed, horizontally. */
|
|
@@ -42,7 +55,17 @@ export interface DragBox {
|
|
|
42
55
|
toY: number;
|
|
43
56
|
}
|
|
44
57
|
|
|
45
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* The drag as it is being made, for the preview rectangle to be drawn from.
|
|
60
|
+
*
|
|
61
|
+
* Its **vertical corners are the window the release will take**, not the two
|
|
62
|
+
* places the hand passed through. The two differ by the baseline a viewer's
|
|
63
|
+
* rules hold: on a mirrored chart zero runs across the middle of the plot, so a
|
|
64
|
+
* drag made wholly inside the reflected half crosses the baseline without ever
|
|
65
|
+
* reaching it, and the release stretches the window back to zero. A rectangle
|
|
66
|
+
* drawn at the height of the hand would promise a band between two intensities
|
|
67
|
+
* that letting go does not give.
|
|
68
|
+
*/
|
|
46
69
|
export interface ZoomSelection extends DragBox {
|
|
47
70
|
/**
|
|
48
71
|
* Whether letting go here would take the y axis with it, which is what the
|
|
@@ -58,9 +81,9 @@ export interface ZoomSelection extends DragBox {
|
|
|
58
81
|
* than by the rectangle itself — so what the preview promises and what letting
|
|
59
82
|
* go actually does are the same test rather than two that have to agree.
|
|
60
83
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
84
|
+
* Which answer a mode gives is `takesYAxis`'s business, so a `select` drag is
|
|
85
|
+
* always a band however far past the baseline it strays, and a `dual` drag
|
|
86
|
+
* becomes a rectangle the moment it has travelled `DUAL_ZOOM_TRAVEL`.
|
|
64
87
|
* @param drag - The drag being made, `null` when none is.
|
|
65
88
|
* @param yScale - The y axis as it is currently zoomed.
|
|
66
89
|
* @param rules - How the y axis behaves and what a drag means, defaulting to a
|
|
@@ -75,24 +98,41 @@ export function zoomSelection(
|
|
|
75
98
|
if (drag === null) return null;
|
|
76
99
|
const {
|
|
77
100
|
baseline = DEFAULT_Y_AXIS_RULES.baseline,
|
|
101
|
+
keepBaseline = DEFAULT_Y_AXIS_RULES.keepBaseline,
|
|
78
102
|
pointsDown = DEFAULT_Y_AXIS_RULES.pointsDown,
|
|
79
103
|
drag: mode = 'xAxis',
|
|
80
104
|
} = rules;
|
|
105
|
+
const zoomsYAxis = takesYAxis(drag, mode, yScale, baseline, pointsDown);
|
|
106
|
+
// Stretched in pixels rather than in values because the rectangle is drawn in
|
|
107
|
+
// pixels and the mapping is monotonic, so including the baseline's own pixel
|
|
108
|
+
// in the span is including the baseline in the window.
|
|
109
|
+
const held = zoomsYAxis && holdsBaseline(mode, keepBaseline);
|
|
110
|
+
const atBaseline = chartPixel(yScale, baseline);
|
|
81
111
|
return {
|
|
82
112
|
fromX: drag.fromX,
|
|
83
|
-
fromY: drag.fromY,
|
|
113
|
+
fromY: held ? Math.min(drag.fromY, drag.toY, atBaseline) : drag.fromY,
|
|
84
114
|
toX: drag.toX,
|
|
85
|
-
toY: drag.toY,
|
|
86
|
-
|
|
87
|
-
// the first pixel of the gesture rather than a full-height band that
|
|
88
|
-
// suddenly becomes one; a select drag never takes it at all.
|
|
89
|
-
zoomsYAxis:
|
|
90
|
-
mode === 'box' ||
|
|
91
|
-
(mode !== 'select' &&
|
|
92
|
-
releasedBeyondBaseline(drag.toY, yScale, baseline, pointsDown)),
|
|
115
|
+
toY: held ? Math.max(drag.fromY, drag.toY, atBaseline) : drag.toY,
|
|
116
|
+
zoomsYAxis,
|
|
93
117
|
};
|
|
94
118
|
}
|
|
95
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Whether a drag has left the level it started on by enough to ask for the y
|
|
122
|
+
* axis as well.
|
|
123
|
+
*
|
|
124
|
+
* Measured as the hand made it, before any clamping to the plot: a drag that ran
|
|
125
|
+
* out over the tick labels travelled however far it travelled. Nothing is
|
|
126
|
+
* latched — the answer is read off the two corners and nothing else — so a
|
|
127
|
+
* reader who drifted down and came back up is promised a band again, and the
|
|
128
|
+
* preview says so while the button is still down.
|
|
129
|
+
* @param drag - The drag being made or released, in user units of the SVG.
|
|
130
|
+
* @returns Whether the height comes along.
|
|
131
|
+
*/
|
|
132
|
+
export function draggedBeyondLevel(drag: DragBox): boolean {
|
|
133
|
+
return Math.abs(drag.toY - drag.fromY) >= DUAL_ZOOM_TRAVEL;
|
|
134
|
+
}
|
|
135
|
+
|
|
96
136
|
/**
|
|
97
137
|
* Whether a drag let go here asks for the y axis as well.
|
|
98
138
|
*
|
|
@@ -178,24 +218,19 @@ export function zoomedDomain(
|
|
|
178
218
|
Math.min(firstX, secondX),
|
|
179
219
|
Math.max(firstX, secondX),
|
|
180
220
|
];
|
|
181
|
-
|
|
182
|
-
// go; the reading gesture takes the height only when it crossed the baseline.
|
|
183
|
-
if (
|
|
184
|
-
mode !== 'box' &&
|
|
185
|
-
!releasedBeyondBaseline(drag.toY, yScale, baseline, pointsDown)
|
|
186
|
-
) {
|
|
221
|
+
if (!takesYAxis(drag, mode, yScale, baseline, pointsDown)) {
|
|
187
222
|
return { x, y: current.y };
|
|
188
223
|
}
|
|
189
224
|
|
|
190
225
|
const fromY = chartValue(yScale, clamp(drag.fromY, plot.top, plot.bottom));
|
|
191
226
|
const toY = chartValue(yScale, clamp(drag.toY, plot.top, plot.bottom));
|
|
192
227
|
// Both ends, so a baseline the data runs *down* from — 100 percent
|
|
193
|
-
// transmittance — is kept as surely as one it stands on.
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// drawn.
|
|
198
|
-
const holdBaseline =
|
|
228
|
+
// transmittance — is kept as surely as one it stands on. Only the reading
|
|
229
|
+
// gesture holds a baseline: a rectangle dragged out deliberately, by the box
|
|
230
|
+
// tool or by a `dual` drag that left its level, is a promise about exactly
|
|
231
|
+
// that rectangle, and a viewer whose axis rules keep its baseline would
|
|
232
|
+
// otherwise see a taller window than the one drawn.
|
|
233
|
+
const holdBaseline = holdsBaseline(mode, keepBaseline);
|
|
199
234
|
const bottom = holdBaseline
|
|
200
235
|
? Math.min(baseline, fromY, toY)
|
|
201
236
|
: Math.min(fromY, toY);
|
|
@@ -233,3 +268,51 @@ export function scaledYAxis(
|
|
|
233
268
|
];
|
|
234
269
|
return y[1] > y[0] ? { x: current.x, y } : current;
|
|
235
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Whether a drag brings the y axis with it, which is the one question the
|
|
274
|
+
* preview and the release must never answer differently.
|
|
275
|
+
*
|
|
276
|
+
* Asked once, here, rather than by each of them: a rectangle that promises a
|
|
277
|
+
* height the release then refuses teaches a reader that the chart is
|
|
278
|
+
* unreliable, and two copies of this test are how that comes about.
|
|
279
|
+
*
|
|
280
|
+
* The box tool takes the height always, so its preview is a rectangle from the
|
|
281
|
+
* first pixel of the gesture rather than a full-height band that suddenly
|
|
282
|
+
* becomes one. A `select` drag never takes it at all: its answer is two numbers
|
|
283
|
+
* on one axis, and a preview that grew into a rectangle would promise a height
|
|
284
|
+
* nobody is ever told about.
|
|
285
|
+
* @param drag - The drag being made or released, in user units of the SVG.
|
|
286
|
+
* @param mode - What a drag on this chart asks for.
|
|
287
|
+
* @param yScale - The y axis as it is currently zoomed.
|
|
288
|
+
* @param baseline - The value the reading gesture is judged against.
|
|
289
|
+
* @param pointsDown - Whether the data hangs below that baseline.
|
|
290
|
+
* @returns Whether the height comes along.
|
|
291
|
+
*/
|
|
292
|
+
/**
|
|
293
|
+
* Whether the window keeps the baseline whatever the drag asked for, which the
|
|
294
|
+
* preview and the release must agree on as surely as they agree on the height.
|
|
295
|
+
*
|
|
296
|
+
* Only the reading gesture holds it. A rectangle dragged out deliberately — by
|
|
297
|
+
* the box tool, or by a `dual` drag that left its level — is a promise about
|
|
298
|
+
* exactly that rectangle.
|
|
299
|
+
* @param mode - What a drag on this chart asks for.
|
|
300
|
+
* @param keepBaseline - Whether the viewer's rules keep it.
|
|
301
|
+
* @returns Whether the baseline stays in the window.
|
|
302
|
+
*/
|
|
303
|
+
function holdsBaseline(mode: DragMode, keepBaseline: boolean): boolean {
|
|
304
|
+
return keepBaseline && mode === 'xAxis';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function takesYAxis(
|
|
308
|
+
drag: DragBox,
|
|
309
|
+
mode: DragMode,
|
|
310
|
+
yScale: ChartScale,
|
|
311
|
+
baseline: number,
|
|
312
|
+
pointsDown: boolean,
|
|
313
|
+
): boolean {
|
|
314
|
+
if (mode === 'box') return true;
|
|
315
|
+
if (mode === 'select') return false;
|
|
316
|
+
if (mode === 'dual') return draggedBeyondLevel(drag);
|
|
317
|
+
return releasedBeyondBaseline(drag.toY, yScale, baseline, pointsDown);
|
|
318
|
+
}
|
|
@@ -36,6 +36,10 @@ export function SelectionRect(props: SelectionRectProps): ReactElement | null {
|
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
38
|
<rect
|
|
39
|
+
// Named, because the shape of this rectangle is the whole promise a drag
|
|
40
|
+
// makes — a band for the x axis alone, the drawn height for both — and
|
|
41
|
+
// the only way to check the promise is to measure it mid-drag.
|
|
42
|
+
data-testid="chart-selection"
|
|
39
43
|
x={rectangle.x}
|
|
40
44
|
y={rectangle.y}
|
|
41
45
|
width={rectangle.width}
|
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
* gesture reporting a range and a click for the same drag.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
KeyboardEvent as ReactKeyboardEvent,
|
|
19
|
+
PointerEvent as ReactPointerEvent,
|
|
20
|
+
} from 'react';
|
|
18
21
|
|
|
22
|
+
import { isEditingField } from '../../panel/core/isEditingField.ts';
|
|
19
23
|
import type { ChartDomain, ZoomRules } from '../core/chartDomain.ts';
|
|
20
24
|
import type { PlotRect } from '../core/chartGeometry.ts';
|
|
21
25
|
import type { ChartScale } from '../core/chartScale.ts';
|
|
@@ -29,6 +33,7 @@ import { isInsidePlot } from './useChartPointer.ts';
|
|
|
29
33
|
|
|
30
34
|
/** One gesture on the SVG, as React hands it over. */
|
|
31
35
|
type PointerHandler = (event: ReactPointerEvent<SVGSVGElement>) => void;
|
|
36
|
+
type KeyboardHandler = (event: ReactKeyboardEvent<SVGSVGElement>) => void;
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* Which keys were held as a click was let go.
|
|
@@ -56,6 +61,41 @@ export interface ChartZoomHandlers {
|
|
|
56
61
|
onPointerUp: PointerHandler;
|
|
57
62
|
onPointerCancel: PointerHandler;
|
|
58
63
|
onDoubleClick: () => void;
|
|
64
|
+
onKeyDown: KeyboardHandler;
|
|
65
|
+
/**
|
|
66
|
+
* So the chart can hold the caret, which is what lets a key reach it.
|
|
67
|
+
*
|
|
68
|
+
* In the tab order rather than merely focusable by a click, because the chart
|
|
69
|
+
* is* the widget here: a reader who navigates by keyboard has no other way to
|
|
70
|
+
* reach it, and a chart nobody can focus answers no key at all. A viewer that
|
|
71
|
+
* answers none takes itself back out of the order.
|
|
72
|
+
*/
|
|
73
|
+
tabIndex: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The key that fits the window to everything drawn, on every chart. */
|
|
77
|
+
export const FIT_KEY = 'f';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Whether a key press asks for the window to be fitted.
|
|
81
|
+
*
|
|
82
|
+
* A key pressed with a modifier belongs to the browser or to the page — Ctrl+F
|
|
83
|
+
* is a search, and a chart that swallowed it to fit itself would be a chart
|
|
84
|
+
* people stop typing near — and a key delivered to a field is part of what is
|
|
85
|
+
* being written. Compared case-insensitively, so a capital arriving from a held
|
|
86
|
+
* shift still fits.
|
|
87
|
+
* @param event - The key press, as React delivers it or as the DOM does.
|
|
88
|
+
* @returns Whether the chart should fit its window.
|
|
89
|
+
*/
|
|
90
|
+
export function asksToFit(
|
|
91
|
+
event: Pick<
|
|
92
|
+
KeyboardEvent,
|
|
93
|
+
'key' | 'ctrlKey' | 'metaKey' | 'altKey' | 'target'
|
|
94
|
+
>,
|
|
95
|
+
): boolean {
|
|
96
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
97
|
+
if (isEditingField(event.target)) return false;
|
|
98
|
+
return event.key.toLowerCase() === FIT_KEY;
|
|
59
99
|
}
|
|
60
100
|
|
|
61
101
|
/** What a host asks to be told, beyond the window it is shown anyway. */
|
package/src/chart/ui/index.ts
CHANGED
|
@@ -47,7 +47,7 @@ export { SelectionRect } from './SelectionRect.tsx';
|
|
|
47
47
|
export type { LegendEntry, TraceLegendProps } from './TraceLegend.tsx';
|
|
48
48
|
export { TraceLegend } from './TraceLegend.tsx';
|
|
49
49
|
export type { ChartClickModifiers } from './chartGestures.ts';
|
|
50
|
-
export { NO_MODIFIERS } from './chartGestures.ts';
|
|
50
|
+
export { FIT_KEY, NO_MODIFIERS, asksToFit } from './chartGestures.ts';
|
|
51
51
|
export { chartSurfaceStyle } from './chartSurface.ts';
|
|
52
52
|
export type { ChartPointer, ChartPointerOptions } from './useChartPointer.ts';
|
|
53
53
|
export {
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
* answer each viewer hands over.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import type {
|
|
31
|
+
import type {
|
|
32
|
+
KeyboardEvent as ReactKeyboardEvent,
|
|
33
|
+
PointerEvent as ReactPointerEvent,
|
|
34
|
+
RefObject,
|
|
35
|
+
} from 'react';
|
|
32
36
|
import { useCallback, useMemo, useState } from 'react';
|
|
33
37
|
|
|
34
38
|
import type {
|
|
@@ -50,6 +54,7 @@ import type {
|
|
|
50
54
|
} from './chartGestures.ts';
|
|
51
55
|
import {
|
|
52
56
|
answeredRelease,
|
|
57
|
+
asksToFit,
|
|
53
58
|
draggedTo,
|
|
54
59
|
releaseDrag,
|
|
55
60
|
startedDrag,
|
|
@@ -147,6 +152,7 @@ export function useChartZoom(options: ChartZoomOptions): ChartZoom {
|
|
|
147
152
|
const pointsDown = yAxis?.pointsDown;
|
|
148
153
|
const dragMode = gestures?.drag ?? DEFAULT_ZOOM_GESTURES.drag;
|
|
149
154
|
const wheel = gestures?.wheel ?? DEFAULT_ZOOM_GESTURES.wheel;
|
|
155
|
+
const keys = gestures?.keys ?? DEFAULT_ZOOM_GESTURES.keys;
|
|
150
156
|
const rules = useMemo<ZoomRules>(
|
|
151
157
|
() => ({ baseline, keepBaseline, pointsDown, drag: dragMode }),
|
|
152
158
|
[baseline, keepBaseline, pointsDown, dragMode],
|
|
@@ -236,6 +242,19 @@ export function useChartZoom(options: ChartZoomOptions): ChartZoom {
|
|
|
236
242
|
[drag, yScale, rules],
|
|
237
243
|
);
|
|
238
244
|
|
|
245
|
+
const onKeyDown = useCallback(
|
|
246
|
+
(event: ReactKeyboardEvent<SVGSVGElement>) => {
|
|
247
|
+
if (!keys || !asksToFit(event)) return;
|
|
248
|
+
// Taken rather than passed on, so the page under a chart does not scroll
|
|
249
|
+
// to its own next heading as well — and so a second chart mounted beside
|
|
250
|
+
// this one is not fitted by a key aimed at this one.
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
event.stopPropagation();
|
|
253
|
+
reset();
|
|
254
|
+
},
|
|
255
|
+
[keys, reset],
|
|
256
|
+
);
|
|
257
|
+
|
|
239
258
|
return {
|
|
240
259
|
domain,
|
|
241
260
|
xScale,
|
|
@@ -248,6 +267,11 @@ export function useChartZoom(options: ChartZoomOptions): ChartZoom {
|
|
|
248
267
|
onPointerUp,
|
|
249
268
|
onPointerCancel,
|
|
250
269
|
onDoubleClick: reset,
|
|
270
|
+
onKeyDown,
|
|
271
|
+
// Out of the tab order again on a chart that answers no key: a stop on the
|
|
272
|
+
// way through a page that does nothing when it is reached is worse than no
|
|
273
|
+
// stop at all.
|
|
274
|
+
tabIndex: keys ? 0 : -1,
|
|
251
275
|
},
|
|
252
276
|
};
|
|
253
277
|
}
|
package/src/ir/core/irMode.ts
CHANGED
|
@@ -168,7 +168,12 @@ export function zoomGestures(
|
|
|
168
168
|
tool: ZoomDragMode = 'xAxis',
|
|
169
169
|
): Required<ZoomGestures> {
|
|
170
170
|
const { boxZoomOnly, wheelZoom } = IR_MODE_RULES[mode];
|
|
171
|
-
|
|
171
|
+
// The shell owns `f`: its own command table runs `resetDomain`, which fits
|
|
172
|
+
// every pane of a stack rather than the one chart the caret is in, and the key
|
|
173
|
+
// is written down in a toolbar tooltip and in the documentation panel. A chart
|
|
174
|
+
// answering it as well would fit one pane and swallow the key before the
|
|
175
|
+
// shell heard it.
|
|
176
|
+
return { drag: boxZoomOnly ? 'box' : tool, wheel: wheelZoom, keys: false };
|
|
172
177
|
}
|
|
173
178
|
|
|
174
179
|
/**
|
package/src/locales/de.json
CHANGED
|
@@ -266,6 +266,7 @@
|
|
|
266
266
|
"periodic.table": "Periodensystem",
|
|
267
267
|
"periodic.group": "Gruppe {group}",
|
|
268
268
|
"periodic.period": "Periode {period}",
|
|
269
|
+
"periodic.allElements": "Alle Elemente",
|
|
269
270
|
"periodic.cellLabel": "{name} ({symbol}, Z = {atomicNumber})",
|
|
270
271
|
"periodic.category.alkali-metal": "Alkalimetall",
|
|
271
272
|
"periodic.category.alkaline-earth-metal": "Erdalkalimetall",
|
package/src/locales/en.json
CHANGED
|
@@ -412,6 +412,7 @@
|
|
|
412
412
|
"periodic.table": "Periodic table",
|
|
413
413
|
"periodic.group": "Group {group}",
|
|
414
414
|
"periodic.period": "Period {period}",
|
|
415
|
+
"periodic.allElements": "All elements",
|
|
415
416
|
"periodic.cellLabel": "{name} ({symbol}, Z = {atomicNumber})",
|
|
416
417
|
"periodic.category.alkali-metal": "Alkali metal",
|
|
417
418
|
"periodic.category.alkaline-earth-metal": "Alkaline earth",
|
package/src/locales/es.json
CHANGED
|
@@ -266,6 +266,7 @@
|
|
|
266
266
|
"periodic.table": "Tabla periódica",
|
|
267
267
|
"periodic.group": "Grupo {group}",
|
|
268
268
|
"periodic.period": "Periodo {period}",
|
|
269
|
+
"periodic.allElements": "Todos los elementos",
|
|
269
270
|
"periodic.cellLabel": "{name} ({symbol}, Z = {atomicNumber})",
|
|
270
271
|
"periodic.category.alkali-metal": "Metal alcalino",
|
|
271
272
|
"periodic.category.alkaline-earth-metal": "Alcalinotérreo",
|
package/src/locales/fr.json
CHANGED
|
@@ -266,6 +266,7 @@
|
|
|
266
266
|
"periodic.table": "Tableau périodique",
|
|
267
267
|
"periodic.group": "Groupe {group}",
|
|
268
268
|
"periodic.period": "Période {period}",
|
|
269
|
+
"periodic.allElements": "Tous les éléments",
|
|
269
270
|
"periodic.cellLabel": "{name} ({symbol}, Z = {atomicNumber})",
|
|
270
271
|
"periodic.category.alkali-metal": "Métal alcalin",
|
|
271
272
|
"periodic.category.alkaline-earth-metal": "Alcalino-terreux",
|
package/src/locales/it.json
CHANGED
|
@@ -412,6 +412,7 @@
|
|
|
412
412
|
"periodic.table": "Tavola periodica",
|
|
413
413
|
"periodic.group": "Gruppo {group}",
|
|
414
414
|
"periodic.period": "Periodo {period}",
|
|
415
|
+
"periodic.allElements": "Tutti gli elementi",
|
|
415
416
|
"periodic.cellLabel": "{name} ({symbol}, Z = {atomicNumber})",
|
|
416
417
|
"periodic.category.alkali-metal": "Metallo alcalino",
|
|
417
418
|
"periodic.category.alkaline-earth-metal": "Alcalino terroso",
|
|
@@ -28,6 +28,18 @@ export interface ElementRange {
|
|
|
28
28
|
additive: boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** One element, and what the click that picked it asked for. */
|
|
32
|
+
export interface ElementPick {
|
|
33
|
+
/**
|
|
34
|
+
* Whether the click asked for this element on top of what was already
|
|
35
|
+
* chosen — Cmd on a Mac, Ctrl elsewhere — rather than in place of it. A tool
|
|
36
|
+
* whose plain click already means something of its own — reading one element
|
|
37
|
+
* into a card — tells the two apart by it, so one table both reads an
|
|
38
|
+
* element and builds a set of them.
|
|
39
|
+
*/
|
|
40
|
+
additive: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
/** A cell of the grid, one-based as CSS grid lines are. */
|
|
32
44
|
export interface Cell {
|
|
33
45
|
/** Grid column, one-based. */
|
|
@@ -16,6 +16,7 @@ import { useState } from 'react';
|
|
|
16
16
|
|
|
17
17
|
import type { Swatch } from '../../color/core/interpolate.ts';
|
|
18
18
|
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
19
|
+
import type { ElementPick } from '../core/layout.ts';
|
|
19
20
|
|
|
20
21
|
import {
|
|
21
22
|
SHELLS_AIR,
|
|
@@ -53,8 +54,11 @@ export interface ElementCellProps {
|
|
|
53
54
|
column: number;
|
|
54
55
|
/** Row of the grid, one-based. */
|
|
55
56
|
row: number;
|
|
56
|
-
/**
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Called with the symbol when the cell is chosen, and with what the click
|
|
59
|
+
* asked for: a plain one, or an additive one — Cmd or Ctrl held.
|
|
60
|
+
*/
|
|
61
|
+
onSelect: (symbol: string, pick: ElementPick) => void;
|
|
58
62
|
/**
|
|
59
63
|
* Third line, under the symbol: the value the tool is showing.
|
|
60
64
|
* @default '' — nothing is written
|
|
@@ -143,8 +147,8 @@ export function ElementCell(props: ElementCellProps): ReactElement {
|
|
|
143
147
|
atomicNumber,
|
|
144
148
|
})}
|
|
145
149
|
aria-pressed={isSelected}
|
|
146
|
-
onClick={() => {
|
|
147
|
-
onSelect(symbol);
|
|
150
|
+
onClick={(event) => {
|
|
151
|
+
onSelect(symbol, { additive: event.metaKey || event.ctrlKey });
|
|
148
152
|
}}
|
|
149
153
|
onPointerEnter={() => {
|
|
150
154
|
setIsHovered(true);
|
|
@@ -23,7 +23,7 @@ import { useResizeObserver } from '../../hooks/ui/useResizeObserver.ts';
|
|
|
23
23
|
import { useChromeT } from '../../i18n/ui/useT.ts';
|
|
24
24
|
import { categorySwatch } from '../core/categories.ts';
|
|
25
25
|
import type { PeriodicElement } from '../core/elements.ts';
|
|
26
|
-
import type { ElementRange } from '../core/layout.ts';
|
|
26
|
+
import type { ElementPick, ElementRange } from '../core/layout.ts';
|
|
27
27
|
import {
|
|
28
28
|
COLUMN_COUNT,
|
|
29
29
|
EMPTY_BLOCK,
|
|
@@ -55,10 +55,12 @@ export interface PeriodicTableProps {
|
|
|
55
55
|
selected?: string;
|
|
56
56
|
/**
|
|
57
57
|
* Called with the symbol of the element that was clicked, and by the arrow
|
|
58
|
-
* keys.
|
|
58
|
+
* keys. The pick says whether the click was additive — Cmd or Ctrl held — so
|
|
59
|
+
* a tool whose plain click already reads one element into a card can let the
|
|
60
|
+
* same table build a set of them.
|
|
59
61
|
* @default undefined — the table is a figure rather than a control
|
|
60
62
|
*/
|
|
61
|
-
onSelect?: (symbol: string) => void;
|
|
63
|
+
onSelect?: (symbol: string, pick: ElementPick) => void;
|
|
62
64
|
/**
|
|
63
65
|
* The colour each cell takes.
|
|
64
66
|
* @default the family colour
|
|
@@ -112,6 +114,13 @@ export interface PeriodicTableProps {
|
|
|
112
114
|
* @default undefined
|
|
113
115
|
*/
|
|
114
116
|
onSelectRange?: (range: ElementRange) => void;
|
|
117
|
+
/**
|
|
118
|
+
* Called when the corner where the two header strips meet is clicked, which
|
|
119
|
+
* is how a reader takes the whole table back. Without it the corner stays
|
|
120
|
+
* empty.
|
|
121
|
+
* @default undefined
|
|
122
|
+
*/
|
|
123
|
+
onSelectAll?: () => void;
|
|
115
124
|
/**
|
|
116
125
|
* Whether to draw the family legend under the grid.
|
|
117
126
|
* @default false
|
|
@@ -158,6 +167,7 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
|
|
|
158
167
|
inset,
|
|
159
168
|
headers = false,
|
|
160
169
|
onSelectRange,
|
|
170
|
+
onSelectAll,
|
|
161
171
|
legend = false,
|
|
162
172
|
markers = true,
|
|
163
173
|
keyboard = true,
|
|
@@ -198,7 +208,7 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
|
|
|
198
208
|
if (next === null) return;
|
|
199
209
|
event.preventDefault();
|
|
200
210
|
cameFromKeyRef.current = true;
|
|
201
|
-
onSelect(next.symbol);
|
|
211
|
+
onSelect(next.symbol, PLAIN_PICK);
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
// One size for the whole table rather than one per cell: a column of values
|
|
@@ -228,7 +238,12 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
|
|
|
228
238
|
style={gridStyle(headers)}
|
|
229
239
|
onKeyDown={handleKeyDown}
|
|
230
240
|
>
|
|
231
|
-
{headers ?
|
|
241
|
+
{headers ? (
|
|
242
|
+
<HeaderStrips
|
|
243
|
+
onSelectRange={onSelectRange}
|
|
244
|
+
onSelectAll={onSelectAll}
|
|
245
|
+
/>
|
|
246
|
+
) : null}
|
|
232
247
|
{inset === undefined ? null : (
|
|
233
248
|
<div style={insetStyle(offset)}>{inset}</div>
|
|
234
249
|
)}
|
|
@@ -290,6 +305,9 @@ function defaultNameOf(element: PeriodicElement): string {
|
|
|
290
305
|
return element.name;
|
|
291
306
|
}
|
|
292
307
|
|
|
308
|
+
/** The arrow keys walk the table to read it; they never add to a set. */
|
|
309
|
+
const PLAIN_PICK: ElementPick = { additive: false };
|
|
310
|
+
|
|
293
311
|
function noop(): void {
|
|
294
312
|
// A table with no `onSelect` is a figure; its cells stay buttons so the
|
|
295
313
|
// keyboard and a screen reader still reach every element.
|