react-cheminfo 0.46.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.
Files changed (88) hide show
  1. package/lib/chart/core/chartDomain.d.ts +14 -0
  2. package/lib/chart/core/chartDomain.d.ts.map +1 -1
  3. package/lib/chart/core/chartDomain.js +1 -0
  4. package/lib/chart/core/chartDomain.js.map +1 -1
  5. package/lib/chart/core/selectionGeometry.d.ts +4 -2
  6. package/lib/chart/core/selectionGeometry.d.ts.map +1 -1
  7. package/lib/chart/core/selectionGeometry.js +4 -2
  8. package/lib/chart/core/selectionGeometry.js.map +1 -1
  9. package/lib/chart/core/zoomDomain.d.ts +11 -1
  10. package/lib/chart/core/zoomDomain.d.ts.map +1 -1
  11. package/lib/chart/core/zoomDomain.js +25 -5
  12. package/lib/chart/core/zoomDomain.js.map +1 -1
  13. package/lib/chart/ui/chartGestures.d.ts +26 -1
  14. package/lib/chart/ui/chartGestures.d.ts.map +1 -1
  15. package/lib/chart/ui/chartGestures.js +21 -0
  16. package/lib/chart/ui/chartGestures.js.map +1 -1
  17. package/lib/chart/ui/index.d.ts +1 -1
  18. package/lib/chart/ui/index.d.ts.map +1 -1
  19. package/lib/chart/ui/index.js +1 -1
  20. package/lib/chart/ui/index.js.map +1 -1
  21. package/lib/chart/ui/useChartZoom.d.ts.map +1 -1
  22. package/lib/chart/ui/useChartZoom.js +17 -1
  23. package/lib/chart/ui/useChartZoom.js.map +1 -1
  24. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  25. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  26. package/lib/ir/core/irMode.d.ts.map +1 -1
  27. package/lib/ir/core/irMode.js +6 -1
  28. package/lib/ir/core/irMode.js.map +1 -1
  29. package/lib/locales/de.json +1 -0
  30. package/lib/locales/en.json +1 -0
  31. package/lib/locales/es.json +1 -0
  32. package/lib/locales/fr.json +1 -0
  33. package/lib/locales/it.json +1 -0
  34. package/lib/periodic/core/index.d.ts +1 -1
  35. package/lib/periodic/core/index.d.ts.map +1 -1
  36. package/lib/periodic/core/layout.d.ts +11 -0
  37. package/lib/periodic/core/layout.d.ts.map +1 -1
  38. package/lib/periodic/core/layout.js.map +1 -1
  39. package/lib/periodic/ui/ElementCell.d.ts +6 -2
  40. package/lib/periodic/ui/ElementCell.d.ts.map +1 -1
  41. package/lib/periodic/ui/ElementCell.js +2 -2
  42. package/lib/periodic/ui/ElementCell.js.map +1 -1
  43. package/lib/periodic/ui/PeriodicTable.d.ts +12 -3
  44. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  45. package/lib/periodic/ui/PeriodicTable.js +5 -3
  46. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  47. package/lib/periodic/ui/PeriodicTableChrome.d.ts +13 -3
  48. package/lib/periodic/ui/PeriodicTableChrome.d.ts.map +1 -1
  49. package/lib/periodic/ui/PeriodicTableChrome.js +18 -6
  50. package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -1
  51. package/lib/split/ui/SplitColumn.d.ts +30 -0
  52. package/lib/split/ui/SplitColumn.d.ts.map +1 -0
  53. package/lib/split/ui/SplitColumn.js +20 -0
  54. package/lib/split/ui/SplitColumn.js.map +1 -0
  55. package/lib/split/ui/SplitRow.d.ts +15 -53
  56. package/lib/split/ui/SplitRow.d.ts.map +1 -1
  57. package/lib/split/ui/SplitRow.js +9 -90
  58. package/lib/split/ui/SplitRow.js.map +1 -1
  59. package/lib/split/ui/index.d.ts +3 -0
  60. package/lib/split/ui/index.d.ts.map +1 -1
  61. package/lib/split/ui/index.js +1 -0
  62. package/lib/split/ui/index.js.map +1 -1
  63. package/lib/split/ui/splitPanes.d.ts +73 -0
  64. package/lib/split/ui/splitPanes.d.ts.map +1 -0
  65. package/lib/split/ui/splitPanes.js +128 -0
  66. package/lib/split/ui/splitPanes.js.map +1 -0
  67. package/package.json +1 -1
  68. package/src/chart/core/chartDomain.ts +15 -0
  69. package/src/chart/core/selectionGeometry.ts +4 -2
  70. package/src/chart/core/zoomDomain.ts +37 -5
  71. package/src/chart/ui/chartGestures.ts +41 -1
  72. package/src/chart/ui/index.ts +1 -1
  73. package/src/chart/ui/useChartZoom.ts +25 -1
  74. package/src/ir/core/irMode.ts +6 -1
  75. package/src/locales/de.json +1 -0
  76. package/src/locales/en.json +1 -0
  77. package/src/locales/es.json +1 -0
  78. package/src/locales/fr.json +1 -0
  79. package/src/locales/it.json +1 -0
  80. package/src/periodic/core/index.ts +1 -1
  81. package/src/periodic/core/layout.ts +12 -0
  82. package/src/periodic/ui/ElementCell.tsx +8 -4
  83. package/src/periodic/ui/PeriodicTable.tsx +23 -5
  84. package/src/periodic/ui/PeriodicTableChrome.tsx +28 -4
  85. package/src/split/ui/SplitColumn.tsx +36 -0
  86. package/src/split/ui/SplitRow.tsx +17 -208
  87. package/src/split/ui/index.ts +3 -0
  88. package/src/split/ui/splitPanes.tsx +267 -0
@@ -1 +1 @@
1
- {"version":3,"file":"SplitRow.js","sourceRoot":"","sources":["../../../src/split/ui/SplitRow.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SplitRow.js","sourceRoot":"","sources":["../../../src/split/ui/SplitRow.tsx"],"names":[],"mappings":";AAWA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAkB,CAAC;AAQ9C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAoB;IAC3C,OAAO,KAAC,UAAU,IAAC,SAAS,EAAC,YAAY,KAAK,KAAK,GAAI,CAAC;AAC1D,CAAC"}
@@ -1,3 +1,6 @@
1
+ export type { SplitColumnProps } from './SplitColumn.tsx';
2
+ export { SplitColumn } from './SplitColumn.tsx';
1
3
  export type { SplitRowProps } from './SplitRow.tsx';
2
4
  export { SplitRow } from './SplitRow.tsx';
5
+ export type { SplitPanesProps } from './splitPanes.tsx';
3
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/split/ui/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/split/ui/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,2 +1,3 @@
1
+ export { SplitColumn } from "./SplitColumn.js";
1
2
  export { SplitRow } from "./SplitRow.js";
2
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/split/ui/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/split/ui/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAmB,CAAC;AAEhD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAgB,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Two figures with a splitter between them, and the share each takes written
3
+ * into the address. The body of `<SplitRow>` and `<SplitColumn>` both.
4
+ *
5
+ * How much room one half deserves beside the other is not ours to decide:
6
+ * somebody embedding the chart wants it nearly whole, somebody teaching off the
7
+ * picker wants the picker nearly whole, and both want the link they hand out to
8
+ * open the way they left it. So the splitter is dragged, the share it comes to
9
+ * rest at goes into `?split=`, and the address is the figure.
10
+ *
11
+ * Two things it cannot do, and must not pretend to: on a screen with no room
12
+ * for two the panes stack and no splitter is drawn; and a link that hid
13
+ * everything one pane holds leaves the other the whole of it rather than a
14
+ * share.
15
+ *
16
+ * The bar itself is react-science's `SplitPane`, and it keeps react-science's
17
+ * look — barely visible on purpose, found by the pointer that changes over it
18
+ * rather than by the eye. All `chrome.css` says about it is that eleven pixels
19
+ * is more gutter than two figures need.
20
+ */
21
+ import type { ReactElement, ReactNode } from 'react';
22
+ import type { SplitRange } from '../core/split.ts';
23
+ /** What both splitters take. */
24
+ export interface SplitPanesProps extends SplitRange {
25
+ /**
26
+ * Share of the box the first pane takes, as a whole percentage, or `null`
27
+ * while the page sits at {@link SplitPanesProps.defaultRatio}.
28
+ */
29
+ ratio: number | null;
30
+ /** The share it opens at when the address names none. */
31
+ defaultRatio: number;
32
+ /**
33
+ * Take the share a drag came to rest at, or `null` when the splitter was
34
+ * double-clicked back to the default.
35
+ */
36
+ onRatio: (ratio: number | null) => void;
37
+ /** The first pane — left, or top — or `null` when a link left nothing in it. */
38
+ start: ReactNode;
39
+ /** The second pane, or `null` when a link left nothing in it. */
40
+ end: ReactNode;
41
+ /**
42
+ * The smallest box that is still split, in pixels — a width for a row, a
43
+ * height for a column. Under it the two panes stack and no splitter is drawn.
44
+ * @default 860 across, 420 down
45
+ */
46
+ stackBelow?: number;
47
+ /**
48
+ * The gutter each pane keeps beside the splitter, in pixels, so neither
49
+ * figure is read against its edge.
50
+ * @default 6
51
+ */
52
+ gutter?: number;
53
+ /**
54
+ * The gap between the two panes once they have stacked, in pixels.
55
+ * @default 16
56
+ */
57
+ stackedGap?: number;
58
+ }
59
+ /** What {@link SplitPanes} takes on top of the public props. */
60
+ interface InternalProps extends SplitPanesProps {
61
+ /** Across the box, or down it. */
62
+ direction: 'horizontal' | 'vertical';
63
+ }
64
+ /**
65
+ * The two panes, divided where the address says.
66
+ *
67
+ * @param props - See {@link SplitPanesProps}.
68
+ * @returns The panes with the splitter between them, the two stacked where
69
+ * there is no room for both, or whichever one a link left, alone.
70
+ */
71
+ export declare function SplitPanes(props: InternalProps): ReactElement | null;
72
+ export {};
73
+ //# sourceMappingURL=splitPanes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splitPanes.d.ts","sourceRoot":"","sources":["../../../src/split/ui/splitPanes.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAiB,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAMpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,gCAAgC;AAChC,MAAM,WAAW,eAAgB,SAAQ,UAAU;IACjD;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,yDAAyD;IACzD,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACxC,gFAAgF;IAChF,KAAK,EAAE,SAAS,CAAC;IACjB,iEAAiE;IACjE,GAAG,EAAE,SAAS,CAAC;IACf;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,gEAAgE;AAChE,UAAU,aAAc,SAAQ,eAAe;IAC7C,kCAAkC;IAClC,SAAS,EAAE,YAAY,GAAG,UAAU,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,GAAG,IAAI,CA6FpE"}
@@ -0,0 +1,128 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from 'react';
3
+ import { SplitPane } from 'react-science/ui';
4
+ import { useResizeObserver } from "../../hooks/ui/useResizeObserver.js";
5
+ import { clampSplit } from "../core/split.js";
6
+ /**
7
+ * The two panes, divided where the address says.
8
+ *
9
+ * @param props - See {@link SplitPanesProps}.
10
+ * @returns The panes with the splitter between them, the two stacked where
11
+ * there is no room for both, or whichever one a link left, alone.
12
+ */
13
+ export function SplitPanes(props) {
14
+ const { direction, ratio, defaultRatio, onRatio, start, end, min, max, stackBelow = direction === 'horizontal' ? STACK_ACROSS : STACK_DOWN, gutter = GUTTER, stackedGap = STACKED_GAP, } = props;
15
+ const across = direction === 'horizontal';
16
+ const box = useRef(null);
17
+ const [size, setSize] = useState(() => viewportSize(across, stackBelow));
18
+ // The share under the pointer, which is not the one in the address: a drag
19
+ // reports every pixel it crosses, and the address is rewritten once, where
20
+ // the splitter is let go.
21
+ const [dragged, setDragged] = useState(null);
22
+ useResizeObserver(box, (measured) => {
23
+ setSize(across ? measured.width : measured.height);
24
+ });
25
+ if (start === null && end === null)
26
+ return null;
27
+ // A column divided by a share needs a box of a definite height to take a
28
+ // share of; in an ordinary page-height one it would measure zero and both
29
+ // panes would collapse, so it stacks instead and says nothing about it.
30
+ const stacked = start === null || end === null || size < stackBelow;
31
+ const startPane = start === null ? null : (_jsx("div", { "data-testid": "split-start", style: stacked ? STACKED_PANE : paneStyle(across, 'start', gutter), children: start }));
32
+ const endPane = end === null ? null : (_jsx("div", { "data-testid": "split-end", style: stacked ? STACKED_PANE : paneStyle(across, 'end', gutter), children: end }));
33
+ if (stacked) {
34
+ return (_jsxs("div", { ref: box, style: { ...STACK, gap: stackedGap }, children: [startPane, endPane] }));
35
+ }
36
+ const share = clampSplit(dragged ?? ratio ?? defaultRatio, { min, max });
37
+ return (_jsx("div", { ref: box, className: "split-row", style: across ? undefined : FILL, children: _jsxs(SplitPane, { direction: direction, controlledSide: "start", size: `${share}%`,
38
+ // Held open, so the only layout a visitor can reach is one the address
39
+ // describes: left to itself a double click collapses a pane, and a link
40
+ // shared from there would open with the pane back. The double click is
41
+ // answered here instead, by returning to the page's own share.
42
+ open: true, onOpenChange: () => {
43
+ setDragged(null);
44
+ onRatio(null);
45
+ }, onSizeChange: (asked) => {
46
+ setDragged(shareOf(asked, share, { min, max }));
47
+ }, onResize: (asked) => {
48
+ const rested = shareOf(asked, share, { min, max });
49
+ setDragged(null);
50
+ onRatio(rested);
51
+ }, children: [startPane, endPane] }) }));
52
+ }
53
+ /**
54
+ * The share a splitter asked for.
55
+ *
56
+ * @param asked - What it reported, a percentage because that is what it was
57
+ * given.
58
+ * @param current - The share in force, kept when the report is not a number.
59
+ * @param range - The range this box is divided in.
60
+ * @returns A whole percentage inside it.
61
+ */
62
+ function shareOf(asked, current, range) {
63
+ const value = Number.parseFloat(asked);
64
+ return Number.isFinite(value) ? clampSplit(value, range) : current;
65
+ }
66
+ /**
67
+ * How big the window is, which is what the box is taken to be until it has been
68
+ * measured. Reading it rather than starting at zero is what keeps a wide page
69
+ * from laying itself out stacked and splitting a frame later, which would build
70
+ * both figures twice on every load.
71
+ *
72
+ * @param across - Whether the box is divided across or down.
73
+ * @param fallback - What to answer where there is no window to read — a test,
74
+ * or a page rendered to a string.
75
+ * @returns The size in pixels.
76
+ */
77
+ function viewportSize(across, fallback) {
78
+ const measure = across ? globalThis.innerWidth : globalThis.innerHeight;
79
+ return typeof measure === 'number' && measure > 0 ? measure : fallback;
80
+ }
81
+ /** The narrowest box still divided across, in pixels. */
82
+ const STACK_ACROSS = 860;
83
+ /**
84
+ * The shortest box still divided down, in pixels. Two figures sharing less than
85
+ * this have about two hundred pixels each, which is under what any of them
86
+ * reads at.
87
+ */
88
+ const STACK_DOWN = 420;
89
+ /** The gutter a pane keeps beside the splitter, in pixels. */
90
+ const GUTTER = 6;
91
+ /** The gap between the panes once they have stacked, in pixels. */
92
+ const STACKED_GAP = 16;
93
+ const STACK = {
94
+ display: 'flex',
95
+ flexDirection: 'column',
96
+ minHeight: 0,
97
+ };
98
+ const STACKED_PANE = {
99
+ display: 'flex',
100
+ flexDirection: 'column',
101
+ minWidth: 0,
102
+ minHeight: 0,
103
+ };
104
+ /** A column takes the height it is given, so its share has something to divide. */
105
+ const FILL = {
106
+ display: 'flex',
107
+ flex: '1 1 0%',
108
+ flexDirection: 'column',
109
+ minHeight: 0,
110
+ };
111
+ function paneStyle(across, side, gutter) {
112
+ const edge = across
113
+ ? side === 'start'
114
+ ? 'paddingRight'
115
+ : 'paddingLeft'
116
+ : side === 'start'
117
+ ? 'paddingBottom'
118
+ : 'paddingTop';
119
+ return {
120
+ display: 'flex',
121
+ flex: '1 1 0%',
122
+ flexDirection: 'column',
123
+ minWidth: 0,
124
+ minHeight: 0,
125
+ [edge]: gutter,
126
+ };
127
+ }
128
+ //# sourceMappingURL=splitPanes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"splitPanes.js","sourceRoot":"","sources":["../../../src/split/ui/splitPanes.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-cheminfo",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "license": "MIT",
5
5
  "description": "React components, hooks and framework-free helpers shared by the cheminfo sites: site chrome, citations, sharing, routing, pedagogy, charts, and the structure, orbital and 3D molecule viewers",
6
6
  "author": "Luc Patiny",
@@ -163,12 +163,27 @@ export interface ZoomGestures {
163
163
  * @default true
164
164
  */
165
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;
166
180
  }
167
181
 
168
182
  /** What the gestures come to when a viewer states none of them. */
169
183
  export const DEFAULT_ZOOM_GESTURES: Required<ZoomGestures> = {
170
184
  drag: 'xAxis',
171
185
  wheel: true,
186
+ keys: true,
172
187
  };
173
188
 
174
189
  /**
@@ -31,8 +31,10 @@ 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 of the drag
35
- * itself, and one that asks for the x axis alone spans the whole plot.
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
39
  * rectangle changing shape — as the pointer crosses the baseline, or as a
38
40
  * `dual` drag leaves the level it started on — is the only warning a user gets
@@ -55,7 +55,17 @@ export interface DragBox {
55
55
  toY: number;
56
56
  }
57
57
 
58
- /** The drag as it is being made, for the preview rectangle to be drawn from. */
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
+ */
59
69
  export interface ZoomSelection extends DragBox {
60
70
  /**
61
71
  * Whether letting go here would take the y axis with it, which is what the
@@ -88,15 +98,22 @@ export function zoomSelection(
88
98
  if (drag === null) return null;
89
99
  const {
90
100
  baseline = DEFAULT_Y_AXIS_RULES.baseline,
101
+ keepBaseline = DEFAULT_Y_AXIS_RULES.keepBaseline,
91
102
  pointsDown = DEFAULT_Y_AXIS_RULES.pointsDown,
92
103
  drag: mode = 'xAxis',
93
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);
94
111
  return {
95
112
  fromX: drag.fromX,
96
- fromY: drag.fromY,
113
+ fromY: held ? Math.min(drag.fromY, drag.toY, atBaseline) : drag.fromY,
97
114
  toX: drag.toX,
98
- toY: drag.toY,
99
- zoomsYAxis: takesYAxis(drag, mode, yScale, baseline, pointsDown),
115
+ toY: held ? Math.max(drag.fromY, drag.toY, atBaseline) : drag.toY,
116
+ zoomsYAxis,
100
117
  };
101
118
  }
102
119
 
@@ -213,7 +230,7 @@ export function zoomedDomain(
213
230
  // tool or by a `dual` drag that left its level, is a promise about exactly
214
231
  // that rectangle, and a viewer whose axis rules keep its baseline would
215
232
  // otherwise see a taller window than the one drawn.
216
- const holdBaseline = keepBaseline && mode === 'xAxis';
233
+ const holdBaseline = holdsBaseline(mode, keepBaseline);
217
234
  const bottom = holdBaseline
218
235
  ? Math.min(baseline, fromY, toY)
219
236
  : Math.min(fromY, toY);
@@ -272,6 +289,21 @@ export function scaledYAxis(
272
289
  * @param pointsDown - Whether the data hangs below that baseline.
273
290
  * @returns Whether the height comes along.
274
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
+
275
307
  function takesYAxis(
276
308
  drag: DragBox,
277
309
  mode: DragMode,
@@ -14,8 +14,12 @@
14
14
  * gesture reporting a range and a click for the same drag.
15
15
  */
16
16
 
17
- import type { PointerEvent as ReactPointerEvent } from 'react';
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. */
@@ -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 { PointerEvent as ReactPointerEvent, RefObject } from 'react';
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
  }
@@ -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
- return { drag: boxZoomOnly ? 'box' : tool, wheel: wheelZoom };
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
  /**
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -15,7 +15,7 @@ export {
15
15
  UNKNOWN_SWATCH,
16
16
  categorySwatch,
17
17
  } from './categories.ts';
18
- export type { Cell, ElementRange } from './layout.ts';
18
+ export type { Cell, ElementPick, ElementRange } from './layout.ts';
19
19
  export {
20
20
  COLUMN_COUNT,
21
21
  EMPTY_BLOCK,
@@ -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
- /** Called with the symbol when the cell is chosen. */
57
- onSelect: (symbol: string) => void;
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);