react-cheminfo 0.33.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.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/lib/axis/core/axisMarks.d.ts +54 -0
  3. package/lib/axis/core/axisMarks.d.ts.map +1 -0
  4. package/lib/axis/core/axisMarks.js +69 -0
  5. package/lib/axis/core/axisMarks.js.map +1 -0
  6. package/lib/axis/core/axisScale.d.ts +31 -0
  7. package/lib/axis/core/axisScale.d.ts.map +1 -0
  8. package/lib/axis/core/axisScale.js +31 -0
  9. package/lib/axis/core/axisScale.js.map +1 -0
  10. package/lib/axis/core/index.d.ts +16 -0
  11. package/lib/axis/core/index.d.ts.map +1 -0
  12. package/lib/axis/core/index.js +13 -0
  13. package/lib/axis/core/index.js.map +1 -0
  14. package/lib/axis/core/logAxisBounds.d.ts +78 -0
  15. package/lib/axis/core/logAxisBounds.d.ts.map +1 -0
  16. package/lib/axis/core/logAxisBounds.js +129 -0
  17. package/lib/axis/core/logAxisBounds.js.map +1 -0
  18. package/lib/axis/ui/AxisScaleControl.d.ts +66 -0
  19. package/lib/axis/ui/AxisScaleControl.d.ts.map +1 -0
  20. package/lib/axis/ui/AxisScaleControl.js +26 -0
  21. package/lib/axis/ui/AxisScaleControl.js.map +1 -0
  22. package/lib/axis/ui/index.d.ts +6 -0
  23. package/lib/axis/ui/index.d.ts.map +1 -0
  24. package/lib/axis/ui/index.js +5 -0
  25. package/lib/axis/ui/index.js.map +1 -0
  26. package/lib/core.d.ts +1 -0
  27. package/lib/core.d.ts.map +1 -1
  28. package/lib/core.js +1 -0
  29. package/lib/core.js.map +1 -1
  30. package/lib/delimited/core/index.d.ts +2 -0
  31. package/lib/delimited/core/index.d.ts.map +1 -1
  32. package/lib/delimited/core/index.js +1 -0
  33. package/lib/delimited/core/index.js.map +1 -1
  34. package/lib/delimited/core/tableRows.d.ts +49 -0
  35. package/lib/delimited/core/tableRows.d.ts.map +1 -0
  36. package/lib/delimited/core/tableRows.js +63 -0
  37. package/lib/delimited/core/tableRows.js.map +1 -0
  38. package/lib/delimited/ui/DelimitedTextPanel.d.ts +9 -0
  39. package/lib/delimited/ui/DelimitedTextPanel.d.ts.map +1 -1
  40. package/lib/delimited/ui/DelimitedTextPanel.js +3 -2
  41. package/lib/delimited/ui/DelimitedTextPanel.js.map +1 -1
  42. package/lib/delimited/ui/TableDataButton.d.ts +96 -0
  43. package/lib/delimited/ui/TableDataButton.d.ts.map +1 -0
  44. package/lib/delimited/ui/TableDataButton.js +39 -0
  45. package/lib/delimited/ui/TableDataButton.js.map +1 -0
  46. package/lib/delimited/ui/index.d.ts +2 -0
  47. package/lib/delimited/ui/index.d.ts.map +1 -1
  48. package/lib/delimited/ui/index.js +1 -0
  49. package/lib/delimited/ui/index.js.map +1 -1
  50. package/lib/i18n/core/chromeCatalog.d.ts +1 -1
  51. package/lib/i18n/core/chromeCatalog.d.ts.map +1 -1
  52. package/lib/locales/de.json +6 -0
  53. package/lib/locales/en.json +6 -0
  54. package/lib/locales/es.json +6 -0
  55. package/lib/locales/fr.json +6 -0
  56. package/lib/locales/it.json +6 -0
  57. package/lib/periodic/core/index.d.ts +1 -1
  58. package/lib/periodic/core/index.d.ts.map +1 -1
  59. package/lib/periodic/core/index.js +1 -1
  60. package/lib/periodic/core/index.js.map +1 -1
  61. package/lib/periodic/core/layout.d.ts +18 -0
  62. package/lib/periodic/core/layout.d.ts.map +1 -1
  63. package/lib/periodic/core/layout.js +9 -0
  64. package/lib/periodic/core/layout.js.map +1 -1
  65. package/lib/periodic/ui/ElementCell.js +15 -3
  66. package/lib/periodic/ui/ElementCell.js.map +1 -1
  67. package/lib/periodic/ui/PeriodicTable.d.ts +12 -1
  68. package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
  69. package/lib/periodic/ui/PeriodicTable.js +25 -3
  70. package/lib/periodic/ui/PeriodicTable.js.map +1 -1
  71. package/lib/ui.d.ts +1 -0
  72. package/lib/ui.d.ts.map +1 -1
  73. package/lib/ui.js +1 -0
  74. package/lib/ui.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/axis/core/axisMarks.ts +114 -0
  77. package/src/axis/core/axisScale.ts +41 -0
  78. package/src/axis/core/index.ts +20 -0
  79. package/src/axis/core/logAxisBounds.ts +198 -0
  80. package/src/axis/ui/AxisScaleControl.tsx +103 -0
  81. package/src/axis/ui/index.ts +9 -0
  82. package/src/core.ts +1 -0
  83. package/src/delimited/core/index.ts +2 -0
  84. package/src/delimited/core/tableRows.ts +93 -0
  85. package/src/delimited/ui/DelimitedTextPanel.tsx +25 -9
  86. package/src/delimited/ui/TableDataButton.tsx +162 -0
  87. package/src/delimited/ui/index.ts +2 -0
  88. package/src/locales/de.json +6 -0
  89. package/src/locales/en.json +6 -0
  90. package/src/locales/es.json +6 -0
  91. package/src/locales/fr.json +6 -0
  92. package/src/locales/it.json +6 -0
  93. package/src/periodic/core/index.ts +1 -0
  94. package/src/periodic/core/layout.ts +19 -0
  95. package/src/periodic/ui/ElementCell.tsx +15 -3
  96. package/src/periodic/ui/PeriodicTable.tsx +45 -1
  97. package/src/ui.ts +1 -0
  98. package/styles/chrome.css +71 -0
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The controls that say how an axis is read.
3
+ */
4
+
5
+ export type {
6
+ AxisScaleAxis,
7
+ AxisScaleControlProps,
8
+ } from './AxisScaleControl.tsx';
9
+ export { AxisScaleControl } from './AxisScaleControl.tsx';
package/src/core.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './about/core/index.ts';
2
+ export * from './axis/core/index.ts';
2
3
  export * from './build/core/index.ts';
3
4
  export * from './chart/core/index.ts';
4
5
  export * from './citation/core/index.ts';
@@ -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
- <textarea
142
- readOnly
143
- value={text}
144
- spellCheck={false}
145
- aria-label={label ?? t('delimited.tableAsText')}
146
- className={Classes.INPUT}
147
- style={{ ...TEXT_STYLE, height }}
148
- onFocus={(event) => event.currentTarget.select()}
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';
@@ -232,6 +232,12 @@
232
232
  "overlay.reset": "Zurücksetzen",
233
233
  "overlay.hoverAName": "Fahren Sie über einen Namen, um zu sehen, was er bewirkt.",
234
234
  "overlay.dismiss": "Schließen",
235
+ "axis.scale": "Skala",
236
+ "axis.xScale": "X-Skala",
237
+ "axis.yScale": "Y-Skala",
238
+ "axis.linear": "Linear",
239
+ "axis.logarithmic": "Log",
240
+ "axis.scaleHelp": "Wie die Achse geteilt ist. Linear bekommt jede Einheit gleich viel Platz, so werden die größten Werte miteinander verglichen; logarithmisch bekommt jeder Faktor zehn gleich viel Platz, so sind die kleinen überhaupt sichtbar.",
235
241
  "delimited.copyTable": "Tabelle kopieren",
236
242
  "delimited.copyOrDownload": "Daten kopieren oder herunterladen",
237
243
  "delimited.close": "Schließen",
@@ -232,6 +232,12 @@
232
232
  "overlay.reset": "Reset",
233
233
  "overlay.hoverAName": "Hover a name for what it does.",
234
234
  "overlay.dismiss": "Dismiss",
235
+ "axis.scale": "Scale",
236
+ "axis.xScale": "X scale",
237
+ "axis.yScale": "Y scale",
238
+ "axis.linear": "Linear",
239
+ "axis.logarithmic": "Log",
240
+ "axis.scaleHelp": "How the axis is spaced. Linear gives every unit the same room, so the largest values are read against each other; logarithmic gives every factor of ten the same room, so the small ones are visible at all.",
235
241
  "delimited.copyTable": "Copy the table",
236
242
  "delimited.copyOrDownload": "Copy or download data",
237
243
  "delimited.close": "Close",
@@ -232,6 +232,12 @@
232
232
  "overlay.reset": "Reiniciar",
233
233
  "overlay.hoverAName": "Pase el puntero sobre un nombre para ver qué hace.",
234
234
  "overlay.dismiss": "Cerrar",
235
+ "axis.scale": "Escala",
236
+ "axis.xScale": "Escala X",
237
+ "axis.yScale": "Escala Y",
238
+ "axis.linear": "Lineal",
239
+ "axis.logarithmic": "Log",
240
+ "axis.scaleHelp": "Cómo se gradúa el eje. En lineal cada unidad ocupa el mismo espacio, de modo que se comparan los valores mayores entre sí; en logarítmica cada factor diez ocupa el mismo espacio, de modo que los pequeños llegan a verse.",
235
241
  "delimited.copyTable": "Copiar la tabla",
236
242
  "delimited.copyOrDownload": "Copiar o descargar los datos",
237
243
  "delimited.close": "Cerrar",
@@ -232,6 +232,12 @@
232
232
  "overlay.reset": "Réinitialiser",
233
233
  "overlay.hoverAName": "Survolez un nom pour savoir ce qu'il fait.",
234
234
  "overlay.dismiss": "Fermer",
235
+ "axis.scale": "Échelle",
236
+ "axis.xScale": "Échelle X",
237
+ "axis.yScale": "Échelle Y",
238
+ "axis.linear": "Linéaire",
239
+ "axis.logarithmic": "Log",
240
+ "axis.scaleHelp": "Comment l'axe est gradué. En linéaire, chaque unité occupe la même place, ce qui compare les grandes valeurs entre elles ; en logarithmique, chaque facteur dix occupe la même place, ce qui rend les petites visibles.",
235
241
  "delimited.copyTable": "Copier le tableau",
236
242
  "delimited.copyOrDownload": "Copier ou télécharger les données",
237
243
  "delimited.close": "Fermer",
@@ -232,6 +232,12 @@
232
232
  "overlay.reset": "Ricomincia",
233
233
  "overlay.hoverAName": "Passare il puntatore su un nome per vedere cosa fa.",
234
234
  "overlay.dismiss": "Chiudi",
235
+ "axis.scale": "Scala",
236
+ "axis.xScale": "Scala X",
237
+ "axis.yScale": "Scala Y",
238
+ "axis.linear": "Lineare",
239
+ "axis.logarithmic": "Log",
240
+ "axis.scaleHelp": "Come è graduato l'asse. In lineare ogni unità occupa lo stesso spazio, così si confrontano fra loro i valori più grandi; in logaritmica ogni fattore dieci occupa lo stesso spazio, così i piccoli si vedono.",
235
241
  "delimited.copyTable": "Copia la tabella",
236
242
  "delimited.copyOrDownload": "Copiare o scaricare i dati",
237
243
  "delimited.close": "Chiudi",
@@ -18,6 +18,7 @@ export {
18
18
  export type { Cell, ElementRange } from './layout.ts';
19
19
  export {
20
20
  COLUMN_COUNT,
21
+ EMPTY_BLOCK,
21
22
  INNER_TRANSITION_MARKERS,
22
23
  INNER_TRANSITION_ROWS,
23
24
  ROW_COUNT,
@@ -84,6 +84,25 @@ let placedCache: ReadonlyArray<{
84
84
  cell: Cell;
85
85
  }> | null = null;
86
86
 
87
+ /**
88
+ * The block of cells the table leaves empty, where no element belongs: logical
89
+ * columns 3 to 12 of the first three periods.
90
+ *
91
+ * It is a tenth of the drawing, in the middle of the top edge, and the one
92
+ * place a tool can write without covering an element — which is why a table
93
+ * printed on a wall puts its title or its key there.
94
+ */
95
+ export const EMPTY_BLOCK: {
96
+ /** Leftmost column of the block, one-based. */
97
+ column: number;
98
+ /** Topmost row of the block, one-based. */
99
+ row: number;
100
+ /** How many columns it spans. */
101
+ columnSpan: number;
102
+ /** How many rows it spans. */
103
+ rowSpan: number;
104
+ } = { column: 3, row: 1, columnSpan: 10, rowSpan: 3 };
105
+
87
106
  /**
88
107
  * The two cells the lanthanoids and the actinoids were lifted out of.
89
108
  *
@@ -263,16 +263,28 @@ const cellStyle = {
263
263
  minWidth: 0,
264
264
  overflow: 'hidden',
265
265
  padding: '1px',
266
+ // The corner the atomic number is pinned into.
267
+ position: 'relative',
266
268
  transition: 'opacity 120ms ease, filter 120ms ease',
267
269
  } as const satisfies CSSProperties;
268
270
 
271
+ /**
272
+ * The atomic number sits out of the flow, in the corner.
273
+ *
274
+ * In the flow it rode on top of a stack the cell centres, so it moved down
275
+ * whenever what is written under the symbol was written smaller — the numbers
276
+ * of a row then sat at as many heights as the row had word lengths. Out of it,
277
+ * the number is in the same corner of every cell, and the symbol is centred in
278
+ * the cell rather than in what is left of it.
279
+ */
269
280
  const numberStyle = {
270
- alignSelf: 'flex-start',
271
- fontSize: 'max(0.33rem, 1.2cqw)',
281
+ fontSize: 'max(0.33rem, 1.1cqw)',
272
282
  fontVariantNumeric: 'tabular-nums',
283
+ left: 'max(1px, 0.14cqw)',
273
284
  lineHeight: 1,
274
285
  opacity: 0.8,
275
- paddingLeft: '0.15cqw',
286
+ position: 'absolute',
287
+ top: 'max(1px, 0.14cqw)',
276
288
  } as const satisfies CSSProperties;
277
289
 
278
290
  const symbolStyle = {
@@ -10,7 +10,12 @@
10
10
  * a site's own richer element record never has to cross into this component.
11
11
  */
12
12
 
13
- import type { CSSProperties, KeyboardEvent, ReactElement } from 'react';
13
+ import type {
14
+ CSSProperties,
15
+ KeyboardEvent,
16
+ ReactElement,
17
+ ReactNode,
18
+ } from 'react';
14
19
  import { useEffect, useRef } from 'react';
15
20
 
16
21
  import type { Swatch } from '../../color/core/interpolate.ts';
@@ -20,6 +25,7 @@ import type { PeriodicElement } from '../core/elements.ts';
20
25
  import type { ElementRange } from '../core/layout.ts';
21
26
  import {
22
27
  COLUMN_COUNT,
28
+ EMPTY_BLOCK,
23
29
  elementByArrowKey,
24
30
  placedElements,
25
31
  } from '../core/layout.ts';
@@ -66,6 +72,17 @@ export interface PeriodicTableProps {
66
72
  * @default every element is
67
73
  */
68
74
  isIncluded?: (element: PeriodicElement) => boolean;
75
+ /**
76
+ * What is written in the block the table leaves empty — columns 3 to 12 of
77
+ * the first three periods, in the middle of the top edge. A tool that reads
78
+ * one element off the table puts what it says about it there, where the eye
79
+ * already is, rather than under the grid.
80
+ *
81
+ * It is sized against the table, like everything else in the grid, so give
82
+ * it type in `em` and it scales with the drawing.
83
+ * @default undefined — the block stays empty
84
+ */
85
+ inset?: ReactNode;
69
86
  /**
70
87
  * Whether to draw the group and period strips.
71
88
  * @default false
@@ -119,6 +136,7 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
119
136
  detailOf,
120
137
  nameOf = defaultNameOf,
121
138
  isIncluded,
139
+ inset,
122
140
  headers = false,
123
141
  onSelectRange,
124
142
  legend = false,
@@ -164,6 +182,9 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
164
182
  onKeyDown={handleKeyDown}
165
183
  >
166
184
  {headers ? <HeaderStrips onSelectRange={onSelectRange} /> : null}
185
+ {inset === undefined ? null : (
186
+ <div style={insetStyle(offset)}>{inset}</div>
187
+ )}
167
188
  {markers ? <InnerTransitionMarkers offset={offset} /> : null}
168
189
  {placedElements().map(({ element, cell }) => (
169
190
  <ElementCell
@@ -188,6 +209,29 @@ export function PeriodicTable(props: PeriodicTableProps): ReactElement {
188
209
  );
189
210
  }
190
211
 
212
+ /**
213
+ * Where what the caller writes in the empty block is placed.
214
+ * @param offset - 1 when the table draws its header strips, 0 otherwise.
215
+ * @returns The style of the slot.
216
+ */
217
+ function insetStyle(offset: number): CSSProperties {
218
+ return {
219
+ alignItems: 'center',
220
+ display: 'flex',
221
+ // Narrower than the block so the writing never touches the cells beside it.
222
+ padding: '0 1cqw',
223
+ gridColumn: `${String(EMPTY_BLOCK.column + offset)} / span ${String(EMPTY_BLOCK.columnSpan)}`,
224
+ gridRow: `${String(EMPTY_BLOCK.row + offset)} / span ${String(EMPTY_BLOCK.rowSpan)}`,
225
+ // A share of the table, with a floor, exactly as a cell sizes its symbol:
226
+ // the block holds the same three rows at every width, so what is written
227
+ // in it has to shrink with them.
228
+ fontSize: 'max(0.6rem, 1.8cqw)',
229
+ lineHeight: 1.35,
230
+ minWidth: 0,
231
+ overflow: 'hidden',
232
+ };
233
+ }
234
+
191
235
  function defaultSwatchOf(element: PeriodicElement): Swatch {
192
236
  return categorySwatch(element.category);
193
237
  }