react-cheminfo 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/download/core/cssPaint.d.ts +27 -0
- package/lib/download/core/cssPaint.d.ts.map +1 -0
- package/lib/download/core/cssPaint.js +63 -0
- package/lib/download/core/cssPaint.js.map +1 -0
- package/lib/download/core/drawingMarkup.d.ts +11 -0
- package/lib/download/core/drawingMarkup.d.ts.map +1 -0
- package/lib/download/core/drawingMarkup.js +32 -0
- package/lib/download/core/drawingMarkup.js.map +1 -0
- package/lib/download/core/figureHtml.d.ts +25 -0
- package/lib/download/core/figureHtml.d.ts.map +1 -0
- package/lib/download/core/figureHtml.js +174 -0
- package/lib/download/core/figureHtml.js.map +1 -0
- package/lib/download/core/figureHtmlMarkup.d.ts +107 -0
- package/lib/download/core/figureHtmlMarkup.d.ts.map +1 -0
- package/lib/download/core/figureHtmlMarkup.js +75 -0
- package/lib/download/core/figureHtmlMarkup.js.map +1 -0
- package/lib/download/core/figureSvg.d.ts.map +1 -1
- package/lib/download/core/figureSvg.js +16 -35
- package/lib/download/core/figureSvg.js.map +1 -1
- package/lib/download/core/figureSvgDocument.d.ts +3 -3
- package/lib/download/core/figureSvgDocument.d.ts.map +1 -1
- package/lib/download/core/figureTarget.d.ts +14 -3
- package/lib/download/core/figureTarget.d.ts.map +1 -1
- package/lib/download/core/figureTarget.js +42 -3
- package/lib/download/core/figureTarget.js.map +1 -1
- package/lib/download/core/index.d.ts +1 -1
- package/lib/download/core/index.d.ts.map +1 -1
- package/lib/download/core/index.js +1 -1
- package/lib/download/core/index.js.map +1 -1
- package/lib/download/core/textLines.d.ts +27 -0
- package/lib/download/core/textLines.d.ts.map +1 -0
- package/lib/download/core/textLines.js +51 -0
- package/lib/download/core/textLines.js.map +1 -0
- package/lib/ecosystem/core/lookup.d.ts +2 -1
- package/lib/ecosystem/core/lookup.d.ts.map +1 -1
- package/lib/ecosystem/core/lookup.js +10 -3
- package/lib/ecosystem/core/lookup.js.map +1 -1
- package/lib/ecosystem/core/types.d.ts +2 -2
- package/lib/ecosystem/core/types.d.ts.map +1 -1
- package/lib/ecosystem/core/unlistedSites.d.ts +9 -0
- package/lib/ecosystem/core/unlistedSites.d.ts.map +1 -0
- package/lib/ecosystem/core/unlistedSites.js +35 -0
- package/lib/ecosystem/core/unlistedSites.js.map +1 -0
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +7 -0
- package/lib/ecosystem/ui/glyphs.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/locales/de.json +55 -1
- package/lib/locales/en.json +55 -1
- package/lib/locales/es.json +55 -1
- package/lib/locales/fr.json +55 -1
- package/lib/locales/it.json +55 -1
- package/lib/periodic/ui/PeriodicTable.d.ts +3 -107
- package/lib/periodic/ui/PeriodicTable.d.ts.map +1 -1
- package/lib/periodic/ui/PeriodicTable.js +5 -82
- package/lib/periodic/ui/PeriodicTable.js.map +1 -1
- package/lib/periodic/ui/PeriodicTableChrome.d.ts.map +1 -1
- package/lib/periodic/ui/PeriodicTableChrome.js +3 -25
- package/lib/periodic/ui/PeriodicTableChrome.js.map +1 -1
- package/lib/periodic/ui/SelectAllCorner.d.ts +23 -0
- package/lib/periodic/ui/SelectAllCorner.d.ts.map +1 -0
- package/lib/periodic/ui/SelectAllCorner.js +40 -0
- package/lib/periodic/ui/SelectAllCorner.js.map +1 -0
- package/lib/periodic/ui/headerStyles.d.ts +17 -0
- package/lib/periodic/ui/headerStyles.d.ts.map +1 -0
- package/lib/periodic/ui/headerStyles.js +19 -0
- package/lib/periodic/ui/headerStyles.js.map +1 -0
- package/lib/periodic/ui/periodicTableProps.d.ts +115 -0
- package/lib/periodic/ui/periodicTableProps.d.ts.map +1 -0
- package/lib/periodic/ui/periodicTableProps.js +8 -0
- package/lib/periodic/ui/periodicTableProps.js.map +1 -0
- package/lib/periodic/ui/tableLayout.d.ts +26 -0
- package/lib/periodic/ui/tableLayout.d.ts.map +1 -0
- package/lib/periodic/ui/tableLayout.js +86 -0
- package/lib/periodic/ui/tableLayout.js.map +1 -0
- package/lib/split/ui/SplitColumn.d.ts +0 -1
- package/lib/split/ui/SplitColumn.d.ts.map +1 -1
- package/lib/split/ui/SplitColumn.js +0 -1
- package/lib/split/ui/SplitColumn.js.map +1 -1
- package/lib/split/ui/SplitRow.d.ts +0 -1
- package/lib/split/ui/SplitRow.d.ts.map +1 -1
- package/lib/split/ui/SplitRow.js +0 -1
- package/lib/split/ui/SplitRow.js.map +1 -1
- package/lib/split/ui/splitPanes.d.ts +12 -1
- package/lib/split/ui/splitPanes.d.ts.map +1 -1
- package/lib/split/ui/splitPanes.js +1 -4
- package/lib/split/ui/splitPanes.js.map +1 -1
- package/lib/taxonomy/core/index.d.ts +8 -0
- package/lib/taxonomy/core/index.d.ts.map +1 -0
- package/lib/taxonomy/core/index.js +5 -0
- package/lib/taxonomy/core/index.js.map +1 -0
- package/lib/taxonomy/core/lineage.d.ts +83 -0
- package/lib/taxonomy/core/lineage.d.ts.map +1 -0
- package/lib/taxonomy/core/lineage.js +126 -0
- package/lib/taxonomy/core/lineage.js.map +1 -0
- package/lib/taxonomy/core/ncbi.d.ts +8 -0
- package/lib/taxonomy/core/ncbi.d.ts.map +1 -0
- package/lib/taxonomy/core/ncbi.js +10 -0
- package/lib/taxonomy/core/ncbi.js.map +1 -0
- package/lib/taxonomy/core/organismName.d.ts +28 -0
- package/lib/taxonomy/core/organismName.d.ts.map +1 -0
- package/lib/taxonomy/core/organismName.js +188 -0
- package/lib/taxonomy/core/organismName.js.map +1 -0
- package/lib/taxonomy/core/organismWords.d.ts +20 -0
- package/lib/taxonomy/core/organismWords.d.ts.map +1 -0
- package/lib/taxonomy/core/organismWords.js +86 -0
- package/lib/taxonomy/core/organismWords.js.map +1 -0
- package/lib/taxonomy/core/ranks.d.ts +51 -0
- package/lib/taxonomy/core/ranks.d.ts.map +1 -0
- package/lib/taxonomy/core/ranks.js +123 -0
- package/lib/taxonomy/core/ranks.js.map +1 -0
- package/lib/taxonomy/ui/OrganismName.d.ts +28 -0
- package/lib/taxonomy/ui/OrganismName.d.ts.map +1 -0
- package/lib/taxonomy/ui/OrganismName.js +28 -0
- package/lib/taxonomy/ui/OrganismName.js.map +1 -0
- package/lib/taxonomy/ui/TaxonLineage.d.ts +67 -0
- package/lib/taxonomy/ui/TaxonLineage.d.ts.map +1 -0
- package/lib/taxonomy/ui/TaxonLineage.js +45 -0
- package/lib/taxonomy/ui/TaxonLineage.js.map +1 -0
- package/lib/taxonomy/ui/index.d.ts +6 -0
- package/lib/taxonomy/ui/index.d.ts.map +1 -0
- package/lib/taxonomy/ui/index.js +4 -0
- package/lib/taxonomy/ui/index.js.map +1 -0
- package/lib/taxonomy/ui/rankLabel.d.ts +17 -0
- package/lib/taxonomy/ui/rankLabel.d.ts.map +1 -0
- package/lib/taxonomy/ui/rankLabel.js +77 -0
- package/lib/taxonomy/ui/rankLabel.js.map +1 -0
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/package.json +1 -1
- package/src/core.ts +1 -0
- package/src/download/core/cssPaint.ts +71 -0
- package/src/download/core/drawingMarkup.ts +35 -0
- package/src/download/core/figureHtml.ts +214 -0
- package/src/download/core/figureHtmlMarkup.ts +187 -0
- package/src/download/core/figureSvg.ts +17 -38
- package/src/download/core/figureSvgDocument.ts +3 -3
- package/src/download/core/figureTarget.ts +41 -8
- package/src/download/core/index.ts +1 -0
- package/src/download/core/textLines.ts +67 -0
- package/src/ecosystem/core/lookup.ts +11 -3
- package/src/ecosystem/core/types.ts +4 -2
- package/src/ecosystem/core/unlistedSites.ts +37 -0
- package/src/ecosystem/ui/glyphs.tsx +44 -0
- package/src/locales/de.json +55 -1
- package/src/locales/en.json +55 -1
- package/src/locales/es.json +55 -1
- package/src/locales/fr.json +55 -1
- package/src/locales/it.json +55 -1
- package/src/periodic/ui/PeriodicTable.tsx +16 -204
- package/src/periodic/ui/PeriodicTableChrome.tsx +3 -24
- package/src/periodic/ui/SelectAllCorner.tsx +65 -0
- package/src/periodic/ui/headerStyles.ts +23 -0
- package/src/periodic/ui/periodicTableProps.ts +117 -0
- package/src/periodic/ui/tableLayout.ts +96 -0
- package/src/split/ui/SplitColumn.tsx +0 -1
- package/src/split/ui/SplitRow.tsx +0 -1
- package/src/split/ui/splitPanes.tsx +21 -5
- package/src/taxonomy/core/index.ts +20 -0
- package/src/taxonomy/core/lineage.ts +161 -0
- package/src/taxonomy/core/ncbi.ts +9 -0
- package/src/taxonomy/core/organismName.ts +217 -0
- package/src/taxonomy/core/organismWords.ts +86 -0
- package/src/taxonomy/core/ranks.ts +134 -0
- package/src/taxonomy/ui/OrganismName.tsx +59 -0
- package/src/taxonomy/ui/TaxonLineage.tsx +183 -0
- package/src/taxonomy/ui/index.ts +5 -0
- package/src/taxonomy/ui/rankLabel.ts +81 -0
- package/src/ui.ts +1 -0
- package/styles/chrome.css +135 -6
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { FocusEvent, ReactElement } from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { headerButtonStyle, headerStyle } from './headerStyles.ts';
|
|
5
|
+
|
|
6
|
+
/** What {@link SelectAllCorner} needs. */
|
|
7
|
+
interface SelectAllCornerProps {
|
|
8
|
+
/** What it does, for the pointer and for a screen reader. */
|
|
9
|
+
title: string;
|
|
10
|
+
/** Called when it is clicked. */
|
|
11
|
+
onClick: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The corner where the two header strips meet, which takes the whole table.
|
|
16
|
+
*
|
|
17
|
+
* It is a control and nothing else, so it stays out of sight until it is
|
|
18
|
+
* pointed at or reached from the keyboard, and it is left off a print and out
|
|
19
|
+
* of a saved figure. The glyph points into the grid it takes, as a
|
|
20
|
+
* spreadsheet's does; it is a mark rather than a word because the corner is a
|
|
21
|
+
* seventeenth of the table's width, and the word differs in every language the
|
|
22
|
+
* chrome speaks.
|
|
23
|
+
* @param props - See {@link SelectAllCornerProps}.
|
|
24
|
+
* @returns The corner.
|
|
25
|
+
*/
|
|
26
|
+
export function SelectAllCorner(props: SelectAllCornerProps): ReactElement {
|
|
27
|
+
const { title, onClick } = props;
|
|
28
|
+
const [pointed, setPointed] = useState(false);
|
|
29
|
+
const [focused, setFocused] = useState(false);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
aria-label={title}
|
|
35
|
+
title={title}
|
|
36
|
+
onClick={onClick}
|
|
37
|
+
style={{
|
|
38
|
+
...headerStyle,
|
|
39
|
+
gridColumn: 1,
|
|
40
|
+
gridRow: 1,
|
|
41
|
+
...headerButtonStyle,
|
|
42
|
+
opacity: pointed || focused ? 1 : 0,
|
|
43
|
+
transition: 'opacity 120ms ease',
|
|
44
|
+
}}
|
|
45
|
+
className="no-print"
|
|
46
|
+
data-figure="chrome"
|
|
47
|
+
onPointerEnter={() => {
|
|
48
|
+
setPointed(true);
|
|
49
|
+
}}
|
|
50
|
+
onPointerLeave={() => {
|
|
51
|
+
setPointed(false);
|
|
52
|
+
}}
|
|
53
|
+
// Only a focus the keyboard gave it: a click focuses it too, and the
|
|
54
|
+
// glyph would otherwise stay up after the pointer has gone.
|
|
55
|
+
onFocus={(event: FocusEvent<HTMLButtonElement>) => {
|
|
56
|
+
setFocused(event.currentTarget.matches(':focus-visible'));
|
|
57
|
+
}}
|
|
58
|
+
onBlur={() => {
|
|
59
|
+
setFocused(false);
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
◢
|
|
63
|
+
</button>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
import { TOKEN } from '../../tokens/core/familyTokens.ts';
|
|
4
|
+
|
|
5
|
+
import { ofWidth } from './unit.ts';
|
|
6
|
+
|
|
7
|
+
/** A group or period number, and the corner where the two strips meet. */
|
|
8
|
+
export const headerStyle = {
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
color: TOKEN.textMuted,
|
|
11
|
+
display: 'flex',
|
|
12
|
+
fontSize: `max(0.45rem, ${ofWidth(1.35)})`,
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
padding: 0,
|
|
15
|
+
} as const satisfies CSSProperties;
|
|
16
|
+
|
|
17
|
+
/** The same, when it is a button: the type and the ground stay the strip's. */
|
|
18
|
+
export const headerButtonStyle = {
|
|
19
|
+
background: 'none',
|
|
20
|
+
border: 'none',
|
|
21
|
+
cursor: 'pointer',
|
|
22
|
+
font: 'inherit',
|
|
23
|
+
} as const satisfies CSSProperties;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a caller hands a `PeriodicTable`.
|
|
3
|
+
*
|
|
4
|
+
* The props live beside the component rather than inside it, so a reader
|
|
5
|
+
* looking for what the table does does not scroll past a page of options.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
9
|
+
|
|
10
|
+
import type { Swatch } from '../../color/core/interpolate.ts';
|
|
11
|
+
import type { PeriodicElement } from '../core/elements.ts';
|
|
12
|
+
import type { ElementPick, ElementRange } from '../core/layout.ts';
|
|
13
|
+
|
|
14
|
+
/** What `PeriodicTable` needs. */
|
|
15
|
+
export interface PeriodicTableProps {
|
|
16
|
+
/**
|
|
17
|
+
* Symbol of the element the tools are pointed at.
|
|
18
|
+
* @default undefined — none is
|
|
19
|
+
*/
|
|
20
|
+
selected?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Called with the symbol of the element that was clicked, and by the arrow
|
|
23
|
+
* keys. The pick says whether the click was additive — Cmd or Ctrl held — so
|
|
24
|
+
* a tool whose plain click already reads one element into a card can let the
|
|
25
|
+
* same table build a set of them.
|
|
26
|
+
* @default undefined — the table is a figure rather than a control
|
|
27
|
+
*/
|
|
28
|
+
onSelect?: (symbol: string, pick: ElementPick) => void;
|
|
29
|
+
/**
|
|
30
|
+
* The colour each cell takes.
|
|
31
|
+
* @default the family colour
|
|
32
|
+
*/
|
|
33
|
+
swatchOf?: (element: PeriodicElement) => Swatch;
|
|
34
|
+
/**
|
|
35
|
+
* What is written under the symbol; an empty string writes nothing.
|
|
36
|
+
* @default nothing is written
|
|
37
|
+
*/
|
|
38
|
+
detailOf?: (element: PeriodicElement) => string;
|
|
39
|
+
/**
|
|
40
|
+
* How an element is named, for the label a screen reader reads. The hook a
|
|
41
|
+
* site translating the table writes its own names through.
|
|
42
|
+
* @default the English name
|
|
43
|
+
*/
|
|
44
|
+
nameOf?: (element: PeriodicElement) => string;
|
|
45
|
+
/**
|
|
46
|
+
* Electrons in each shell of an element, innermost first, written down the
|
|
47
|
+
* right-hand edge of its cell. The energy levels, which a table is also read
|
|
48
|
+
* for: none of ours knows them, so the tool that does says so here.
|
|
49
|
+
* @default undefined — no cell writes any
|
|
50
|
+
*/
|
|
51
|
+
shellsOf?: (element: PeriodicElement) => readonly number[] | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Whether an element is inside the set the tool is showing. Everything
|
|
54
|
+
* outside it is dimmed rather than removed, so the table keeps its shape.
|
|
55
|
+
* @default every element is
|
|
56
|
+
*/
|
|
57
|
+
isIncluded?: (element: PeriodicElement) => boolean;
|
|
58
|
+
/**
|
|
59
|
+
* What is written in the block the table leaves empty — columns 3 to 12 of
|
|
60
|
+
* the first three periods, in the middle of the top edge. A tool that reads
|
|
61
|
+
* one element off the table puts what it says about it there, where the eye
|
|
62
|
+
* already is, rather than under the grid.
|
|
63
|
+
*
|
|
64
|
+
* It is sized against the table, like everything else in the grid, so give
|
|
65
|
+
* it type in `em` and it scales with the drawing.
|
|
66
|
+
* @default undefined — the block stays empty
|
|
67
|
+
*/
|
|
68
|
+
inset?: ReactNode;
|
|
69
|
+
/**
|
|
70
|
+
* Whether to draw the group and period strips.
|
|
71
|
+
* @default false
|
|
72
|
+
*/
|
|
73
|
+
headers?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Called when a whole group or period header is clicked. The run says
|
|
76
|
+
* whether the click was additive — Cmd or Ctrl held — so a tool can let a
|
|
77
|
+
* reader put two periods on one chart. Without it the strips are labels
|
|
78
|
+
* rather than buttons.
|
|
79
|
+
* @default undefined
|
|
80
|
+
*/
|
|
81
|
+
onSelectRange?: (range: ElementRange) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Called when the corner where the two header strips meet is clicked, which
|
|
84
|
+
* is how a reader takes the whole table back. The corner shows its glyph only
|
|
85
|
+
* while it is pointed at or reached from the keyboard. Without it the corner
|
|
86
|
+
* stays empty.
|
|
87
|
+
* @default undefined
|
|
88
|
+
*/
|
|
89
|
+
onSelectAll?: () => void;
|
|
90
|
+
/**
|
|
91
|
+
* Whether to draw the family legend under the grid.
|
|
92
|
+
* @default false
|
|
93
|
+
*/
|
|
94
|
+
legend?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Whether the dashed markers stand where the two inner-transition series were
|
|
97
|
+
* lifted out of the main block.
|
|
98
|
+
* @default true
|
|
99
|
+
*/
|
|
100
|
+
markers?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Whether the arrow keys walk the grid: down from carbon is silicon, and
|
|
103
|
+
* right from the end of a period is the start of the next.
|
|
104
|
+
* @default true
|
|
105
|
+
*/
|
|
106
|
+
keyboard?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Called on pointer enter with the symbol, and on leave with `null`.
|
|
109
|
+
* @default undefined
|
|
110
|
+
*/
|
|
111
|
+
onHover?: (symbol: string | null) => void;
|
|
112
|
+
/**
|
|
113
|
+
* Class of the outermost element.
|
|
114
|
+
* @default undefined
|
|
115
|
+
*/
|
|
116
|
+
className?: string;
|
|
117
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the table is laid out: the box it measures, the grid its cells sit on,
|
|
3
|
+
* and the block a caller may write in.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { CSSProperties } from 'react';
|
|
7
|
+
|
|
8
|
+
import { COLUMN_COUNT, EMPTY_BLOCK } from '../core/layout.ts';
|
|
9
|
+
|
|
10
|
+
import { ROW_SHARE } from './cellType.ts';
|
|
11
|
+
import { ofWidth } from './unit.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Where what the caller writes in the empty block is placed.
|
|
15
|
+
* @param offset - 1 when the table draws its header strips, 0 otherwise.
|
|
16
|
+
* @returns The style of the slot.
|
|
17
|
+
*/
|
|
18
|
+
export function insetStyle(offset: number): CSSProperties {
|
|
19
|
+
return {
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
display: 'flex',
|
|
22
|
+
// A column of air on each side, so the writing reads as sitting in the
|
|
23
|
+
// block rather than as running into the cells beside it.
|
|
24
|
+
padding: `0 ${ofWidth(2.4)}`,
|
|
25
|
+
gridColumn: `${String(EMPTY_BLOCK.column + offset)} / span ${String(EMPTY_BLOCK.columnSpan)}`,
|
|
26
|
+
gridRow: `${String(EMPTY_BLOCK.row + offset)} / span ${String(EMPTY_BLOCK.rowSpan)}`,
|
|
27
|
+
// A share of the table, with a floor, exactly as a cell sizes its symbol:
|
|
28
|
+
// the block holds the same three rows at every width, so what is written
|
|
29
|
+
// in it has to shrink with them.
|
|
30
|
+
fontSize: `max(0.6rem, ${ofWidth(1.8)})`,
|
|
31
|
+
lineHeight: 1.35,
|
|
32
|
+
minWidth: 0,
|
|
33
|
+
overflow: 'hidden',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Narrowest the table is ever drawn; under it the type stops being readable. */
|
|
38
|
+
const MIN_WIDTH = 280;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How tall one row of elements is, as a share of the table's width.
|
|
42
|
+
*
|
|
43
|
+
* A column is about 5.2% of that width, so a cell is a third taller than it is
|
|
44
|
+
* wide — the proportion of a wall chart, and the room the three bands of a
|
|
45
|
+
* cell need to be read from the back of a room. It is the same height in every
|
|
46
|
+
* table: a cell keeps the band it writes a value in whether or not that table
|
|
47
|
+
* writes one.
|
|
48
|
+
*/
|
|
49
|
+
const ROW_HEIGHT = ofWidth(ROW_SHARE);
|
|
50
|
+
|
|
51
|
+
/** The band the two inner-transition series were lifted out across. */
|
|
52
|
+
const SERIES_GAP = `max(6px, ${ofWidth(1.1)})`;
|
|
53
|
+
|
|
54
|
+
export const rootStyle = {
|
|
55
|
+
display: 'flex',
|
|
56
|
+
flexDirection: 'column',
|
|
57
|
+
gap: 8,
|
|
58
|
+
minWidth: MIN_WIDTH,
|
|
59
|
+
// Everything inside — the height of a row as much as the type in a cell — is
|
|
60
|
+
// a share of this box rather than of the page, so the same table reads at
|
|
61
|
+
// 280px beside a chart and fills a lecture-hall screen at twice that. The
|
|
62
|
+
// share is measured (see `unit.ts`); the container is what the `1cqw`
|
|
63
|
+
// fallback and a site's own `@container` rules are answered by, and it keeps
|
|
64
|
+
// this box's width independent of what is written inside it.
|
|
65
|
+
containerType: 'inline-size',
|
|
66
|
+
} as const satisfies CSSProperties;
|
|
67
|
+
|
|
68
|
+
const baseGridStyle = {
|
|
69
|
+
display: 'grid',
|
|
70
|
+
gap: 2,
|
|
71
|
+
minWidth: MIN_WIDTH,
|
|
72
|
+
width: '100%',
|
|
73
|
+
} as const satisfies CSSProperties;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The grid the cells are placed on.
|
|
77
|
+
* @param headers - Whether a leading column and row hold the period and group
|
|
78
|
+
* numbers.
|
|
79
|
+
* @returns The style of the grid.
|
|
80
|
+
*/
|
|
81
|
+
export function gridStyle(headers: boolean): CSSProperties {
|
|
82
|
+
// The eighth row is the gap the inner-transition series are lifted out into.
|
|
83
|
+
const rows = `repeat(7, ${ROW_HEIGHT}) ${SERIES_GAP} repeat(2, ${ROW_HEIGHT})`;
|
|
84
|
+
if (!headers) {
|
|
85
|
+
return {
|
|
86
|
+
...baseGridStyle,
|
|
87
|
+
gridTemplateColumns: `repeat(${String(COLUMN_COUNT)}, minmax(0, 1fr))`,
|
|
88
|
+
gridTemplateRows: rows,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
...baseGridStyle,
|
|
93
|
+
gridTemplateColumns: `max(14px, ${ofWidth(2.4)}) repeat(${String(COLUMN_COUNT)}, minmax(0, 1fr))`,
|
|
94
|
+
gridTemplateRows: `max(12px, ${ofWidth(2)}) ${rows}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -26,7 +26,6 @@ export type SplitColumnProps = SplitPanesProps;
|
|
|
26
26
|
* no height to divide, it measures nothing, says nothing about it, and simply
|
|
27
27
|
* stacks the two panes with no splitter, which is what an ordinary page wanted
|
|
28
28
|
* anyway.
|
|
29
|
-
*
|
|
30
29
|
* @param props - See {@link SplitColumnProps}.
|
|
31
30
|
* @returns The two panes with the splitter between them, the two stacked where
|
|
32
31
|
* there is no height to divide, or whichever one a link left, alone.
|
|
@@ -24,7 +24,6 @@ export type SplitRowProps = SplitPanesProps;
|
|
|
24
24
|
* of forms dragged past about half the row does not grow: the labels stay put
|
|
25
25
|
* and the fields stretch to a width nobody types into, so the room becomes
|
|
26
26
|
* empty card. The figure beside it is what the drag exists to enlarge.
|
|
27
|
-
*
|
|
28
27
|
* @param props - See {@link SplitRowProps}.
|
|
29
28
|
* @returns The two panes with the splitter between them, the two stacked on a
|
|
30
29
|
* narrow screen, or whichever one a link left on the page, alone.
|
|
@@ -13,6 +13,18 @@
|
|
|
13
13
|
* everything one pane holds leaves the other the whole of it rather than a
|
|
14
14
|
* share.
|
|
15
15
|
*
|
|
16
|
+
* **Several bars on one page make a grid, and a grid has to read as one.** Three
|
|
17
|
+
* panes are one of these nested inside the other, and the shares are numbered in
|
|
18
|
+
* reading order — `split` for the first bar a reader meets going left to right
|
|
19
|
+
* and then down, `split2` for the next. Two rules keep that from coming apart:
|
|
20
|
+
* **at most two bars on a page**, because a third is a window manager rather
|
|
21
|
+
* than a tool and every bar is one more thing to understand before the page can
|
|
22
|
+
* be used; and **never a bar across two columns of the same row**, because two
|
|
23
|
+
* of them at independent heights do not read as a grid, they read as a page that
|
|
24
|
+
* has come apart. Divide the column that holds the figures and leave its
|
|
25
|
+
* neighbours whole — or, where both genuinely want the same line, give them one
|
|
26
|
+
* share and one parameter so it runs straight across.
|
|
27
|
+
*
|
|
16
28
|
* The bar itself is react-science's `SplitPane`, and it keeps react-science's
|
|
17
29
|
* look — barely visible on purpose, found by the pointer that changes over it
|
|
18
30
|
* rather than by the eye. All `chrome.css` says about it is that eleven pixels
|
|
@@ -73,7 +85,6 @@ interface InternalProps extends SplitPanesProps {
|
|
|
73
85
|
|
|
74
86
|
/**
|
|
75
87
|
* The two panes, divided where the address says.
|
|
76
|
-
*
|
|
77
88
|
* @param props - See {@link SplitPanesProps}.
|
|
78
89
|
* @returns The panes with the splitter between them, the two stacked where
|
|
79
90
|
* there is no room for both, or whichever one a link left, alone.
|
|
@@ -142,8 +153,15 @@ export function SplitPanes(props: InternalProps): ReactElement | null {
|
|
|
142
153
|
|
|
143
154
|
return (
|
|
144
155
|
// `split-row` is what `chrome.css` reaches the bar through: it is
|
|
145
|
-
// react-science's own element and carries no class of ours.
|
|
146
|
-
|
|
156
|
+
// react-science's own element and carries no class of ours. A column says
|
|
157
|
+
// which way it divides as well, because its bar is that same line turned a
|
|
158
|
+
// quarter: there it is the height that is narrowed, and the width that is
|
|
159
|
+
// left to run the pane.
|
|
160
|
+
<div
|
|
161
|
+
ref={box}
|
|
162
|
+
className={across ? 'split-row' : 'split-row split-row--down'}
|
|
163
|
+
style={across ? undefined : FILL}
|
|
164
|
+
>
|
|
147
165
|
<SplitPane
|
|
148
166
|
direction={direction}
|
|
149
167
|
controlledSide="start"
|
|
@@ -175,7 +193,6 @@ export function SplitPanes(props: InternalProps): ReactElement | null {
|
|
|
175
193
|
|
|
176
194
|
/**
|
|
177
195
|
* The share a splitter asked for.
|
|
178
|
-
*
|
|
179
196
|
* @param asked - What it reported, a percentage because that is what it was
|
|
180
197
|
* given.
|
|
181
198
|
* @param current - The share in force, kept when the report is not a number.
|
|
@@ -196,7 +213,6 @@ function shareOf(
|
|
|
196
213
|
* measured. Reading it rather than starting at zero is what keeps a wide page
|
|
197
214
|
* from laying itself out stacked and splitting a frame later, which would build
|
|
198
215
|
* both figures twice on every load.
|
|
199
|
-
*
|
|
200
216
|
* @param across - Whether the box is divided across or down.
|
|
201
217
|
* @param fallback - What to answer where there is no window to read — a test,
|
|
202
218
|
* or a page rendered to a string.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type { Taxon, TaxonLineage, TaxonNode } from './lineage.ts';
|
|
2
|
+
export {
|
|
3
|
+
genusOrBelow,
|
|
4
|
+
isPlaceholderTaxon,
|
|
5
|
+
lineageFromNodes,
|
|
6
|
+
lineageFromRanks,
|
|
7
|
+
principalLineage,
|
|
8
|
+
} from './lineage.ts';
|
|
9
|
+
export { ncbiTaxonomyUrl } from './ncbi.ts';
|
|
10
|
+
export type { OrganismNamePart } from './organismName.ts';
|
|
11
|
+
export { splitOrganismName } from './organismName.ts';
|
|
12
|
+
export type { TaxonRank } from './ranks.ts';
|
|
13
|
+
export {
|
|
14
|
+
PRINCIPAL_RANKS,
|
|
15
|
+
TAXON_RANKS,
|
|
16
|
+
isAboveGenus,
|
|
17
|
+
isGenusOrBelow,
|
|
18
|
+
isPrincipalRank,
|
|
19
|
+
rankOrder,
|
|
20
|
+
} from './ranks.ts';
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lineage: the taxa from the top of the tree of life down to one of them.
|
|
3
|
+
*
|
|
4
|
+
* It is read three ways, because the family meets it in three shapes: a map of
|
|
5
|
+
* NCBI nodes each pointing at its parent (what a database built on the NCBI
|
|
6
|
+
* dump answers), a record of names by rank (what octochemdb stores), or a list
|
|
7
|
+
* already in order.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
TAXON_RANKS,
|
|
12
|
+
isGenusOrBelow,
|
|
13
|
+
isPrincipalRank,
|
|
14
|
+
rankOrder,
|
|
15
|
+
} from './ranks.ts';
|
|
16
|
+
|
|
17
|
+
/** One taxon of a lineage. */
|
|
18
|
+
export interface Taxon {
|
|
19
|
+
/** The rank, as NCBI writes it: `genus`, `no rank`, `clade`… */
|
|
20
|
+
rank: string;
|
|
21
|
+
/** The scientific name. */
|
|
22
|
+
name: string;
|
|
23
|
+
/**
|
|
24
|
+
* The NCBI Taxonomy id.
|
|
25
|
+
* @default undefined — the taxon is known by its name alone
|
|
26
|
+
*/
|
|
27
|
+
taxId?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The taxa from the top of the tree down to one of them, in that order. */
|
|
31
|
+
export type TaxonLineage = readonly Taxon[];
|
|
32
|
+
|
|
33
|
+
/** A taxon of the NCBI tree, as a map of nodes holds it. */
|
|
34
|
+
export interface TaxonNode {
|
|
35
|
+
/** The scientific name. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** The rank, as NCBI writes it. */
|
|
38
|
+
rank: string;
|
|
39
|
+
/** The id of the taxon above; the root is its own parent. */
|
|
40
|
+
parentId: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The taxa a lineage is summarised by: those of a principal rank (domain,
|
|
45
|
+
* kingdom, phylum, class, order, family, genus, species), and the last one
|
|
46
|
+
* whatever its rank, since it is the taxon the lineage leads to.
|
|
47
|
+
*
|
|
48
|
+
* An NCBI placeholder for an unidentified species ('Streptomyces sp.') says
|
|
49
|
+
* no more than its genus, so it is left out and the genus is the leaf. The
|
|
50
|
+
* root, 'cellular organisms', the clades and the steps of no rank above the
|
|
51
|
+
* leaf never show.
|
|
52
|
+
* @param lineage - The lineage, from the top down.
|
|
53
|
+
* @returns The taxa kept, in the same order.
|
|
54
|
+
*/
|
|
55
|
+
export function principalLineage(lineage: TaxonLineage): Taxon[] {
|
|
56
|
+
const kept: Taxon[] = [];
|
|
57
|
+
let last = lineage.length - 1;
|
|
58
|
+
if (last > 0 && isPlaceholderTaxon(lineage[last] as Taxon)) {
|
|
59
|
+
// The genus becomes the leaf, past 'unclassified Streptomyces'.
|
|
60
|
+
last--;
|
|
61
|
+
while (last > 0 && !isPrincipalRank((lineage[last] as Taxon).rank)) last--;
|
|
62
|
+
}
|
|
63
|
+
for (let index = 0; index <= last; index++) {
|
|
64
|
+
const taxon = lineage[index] as Taxon;
|
|
65
|
+
if (index < last && isPlaceholderTaxon(taxon)) continue;
|
|
66
|
+
if (index === last || isPrincipalRank(taxon.rank)) kept.push(taxon);
|
|
67
|
+
}
|
|
68
|
+
return kept;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether a taxon is an NCBI placeholder for an unidentified species of a
|
|
73
|
+
* genus: a species named the genus and 'sp.', such as 'Streptomyces sp.'.
|
|
74
|
+
* One named for a strain ('Streptomyces sp. CNQ-509') is a taxon of its own.
|
|
75
|
+
* @param taxon - The taxon.
|
|
76
|
+
* @returns Whether it is one.
|
|
77
|
+
*/
|
|
78
|
+
export function isPlaceholderTaxon(taxon: Taxon): boolean {
|
|
79
|
+
return taxon.rank === 'species' && /\ssp\.$/.test(taxon.name);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The lineage of a taxon, walked up a map of NCBI nodes to the root.
|
|
84
|
+
* @param nodes - The nodes, by id: a record keyed by the id written as a
|
|
85
|
+
* string, as JSON delivers it, or a map keyed by the number.
|
|
86
|
+
* @param taxId - The taxon to start from.
|
|
87
|
+
* @returns Every taxon from the root down to it, each with its id; empty when
|
|
88
|
+
* the map does not hold the taxon.
|
|
89
|
+
*/
|
|
90
|
+
export function lineageFromNodes(
|
|
91
|
+
nodes: Readonly<Record<string, TaxonNode>> | ReadonlyMap<number, TaxonNode>,
|
|
92
|
+
taxId: number,
|
|
93
|
+
): Taxon[] {
|
|
94
|
+
const read =
|
|
95
|
+
nodes instanceof Map
|
|
96
|
+
? (id: number) => nodes.get(id)
|
|
97
|
+
: (id: number) =>
|
|
98
|
+
(nodes as Readonly<Record<string, TaxonNode>>)[String(id)];
|
|
99
|
+
const upward: Taxon[] = [];
|
|
100
|
+
const seen = new Set<number>();
|
|
101
|
+
let id = taxId;
|
|
102
|
+
while (!seen.has(id)) {
|
|
103
|
+
const node = read(id);
|
|
104
|
+
if (node === undefined) break;
|
|
105
|
+
seen.add(id);
|
|
106
|
+
upward.push({ rank: node.rank, name: node.name, taxId: id });
|
|
107
|
+
id = node.parentId;
|
|
108
|
+
}
|
|
109
|
+
return upward.toReversed();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The lineage written by a record of names by rank, the shape octochemdb
|
|
114
|
+
* stores (`{ kingdom: 'Fungi', genus: 'Penicillium', … }`).
|
|
115
|
+
*
|
|
116
|
+
* A key is matched whatever its case, so `superKingdom` reads as
|
|
117
|
+
* `superkingdom`; a key that names no rank, and an empty name, are left out.
|
|
118
|
+
* @param ranks - The names, by rank.
|
|
119
|
+
* @returns The taxa, from the top down.
|
|
120
|
+
*/
|
|
121
|
+
export function lineageFromRanks(
|
|
122
|
+
ranks: Readonly<Record<string, string | null | undefined>>,
|
|
123
|
+
): Taxon[] {
|
|
124
|
+
const byRank = new Map<string, string>();
|
|
125
|
+
for (const key of Object.keys(ranks)) {
|
|
126
|
+
const name = ranks[key]?.trim();
|
|
127
|
+
if (name === undefined || name === '') continue;
|
|
128
|
+
const rank = key.toLowerCase();
|
|
129
|
+
if (rankOrder(rank) !== undefined) byRank.set(rank, name);
|
|
130
|
+
}
|
|
131
|
+
const lineage: Taxon[] = [];
|
|
132
|
+
for (const rank of TAXON_RANKS) {
|
|
133
|
+
const name = byRank.get(rank);
|
|
134
|
+
if (name !== undefined) lineage.push({ rank, name });
|
|
135
|
+
}
|
|
136
|
+
return lineage;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Which taxa of a lineage sit at the genus or under it — the ones whose names
|
|
141
|
+
* nomenclature sets in italics.
|
|
142
|
+
*
|
|
143
|
+
* A taxon with no rank (a clade, a strain NCBI left unranked) takes the place
|
|
144
|
+
* of the taxa above it: under a genus it is one of the genus's, above it it is
|
|
145
|
+
* a clade like Embryophyta, written upright.
|
|
146
|
+
* @param lineage - The lineage, from the top down.
|
|
147
|
+
* @returns One flag per taxon.
|
|
148
|
+
*/
|
|
149
|
+
export function genusOrBelow(lineage: TaxonLineage): boolean[] {
|
|
150
|
+
const flags: boolean[] = [];
|
|
151
|
+
let below = false;
|
|
152
|
+
for (const taxon of lineage) {
|
|
153
|
+
if (isGenusOrBelow(taxon.rank)) {
|
|
154
|
+
below = true;
|
|
155
|
+
} else if (rankOrder(taxon.rank) !== undefined) {
|
|
156
|
+
below = false;
|
|
157
|
+
}
|
|
158
|
+
flags.push(below);
|
|
159
|
+
}
|
|
160
|
+
return flags;
|
|
161
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page of a taxon in the NCBI Taxonomy Browser.
|
|
3
|
+
* @param taxId - The NCBI Taxonomy id.
|
|
4
|
+
* @returns Its address, e.g.
|
|
5
|
+
* `https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id=5076`.
|
|
6
|
+
*/
|
|
7
|
+
export function ncbiTaxonomyUrl(taxId: number): string {
|
|
8
|
+
return `https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id=${encodeURIComponent(String(taxId))}`;
|
|
9
|
+
}
|