diff --git a/src/h5web/toolbar/HeatmapToolbar.tsx b/src/h5web/toolbar/HeatmapToolbar.tsx
index 344cec7c3..d4101ff79 100644
--- a/src/h5web/toolbar/HeatmapToolbar.tsx
+++ b/src/h5web/toolbar/HeatmapToolbar.tsx
@@ -2,7 +2,7 @@ import type { ReactElement } from 'react';
import { MdAspectRatio, MdGridOn } from 'react-icons/md';
import ToggleBtn from './controls/ToggleBtn';
import { useHeatmapConfig } from '../vis-packs/core/heatmap/config';
-import DomainSlider from './controls/DomainSlider';
+import DomainSlider from './controls/DomainSlider/DomainSlider';
import SnapshotButton from './controls/SnapshotButton';
import Separator from './Separator';
import Toolbar from './Toolbar';
@@ -32,8 +32,8 @@ function HeatmapToolbar(): ReactElement {
{dataDomain && (
)}
diff --git a/src/h5web/toolbar/OverflowMenu.module.css b/src/h5web/toolbar/OverflowMenu.module.css
index 319ca3025..138be5c8d 100644
--- a/src/h5web/toolbar/OverflowMenu.module.css
+++ b/src/h5web/toolbar/OverflowMenu.module.css
@@ -16,15 +16,16 @@
}
.menu {
- position: absolute;
- bottom: 0;
+ composes: popup from './Toolbar.module.css';
right: 0.25rem;
- transform: translateY(100%);
+}
+
+.menuList {
+ composes: popupInner from './Toolbar.module.css';
+ display: grid;
+ grid-gap: 0.25rem;
margin: 0;
padding: 0.375rem 0.25rem;
- background-color: var(--secondary-light-bg);
- box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 1px,
- rgba(0, 0, 0, 0.1) 0px 4px 11px;
list-style-type: none;
}
diff --git a/src/h5web/toolbar/OverflowMenu.tsx b/src/h5web/toolbar/OverflowMenu.tsx
index 9f84e611e..217555189 100644
--- a/src/h5web/toolbar/OverflowMenu.tsx
+++ b/src/h5web/toolbar/OverflowMenu.tsx
@@ -46,18 +46,20 @@ function OverflowMenu(props: Props): ReactElement {
-
+
+ {Children.map(children, (child) => (
+ // Render cloned child (React elements don't like to be moved around)
+ - {cloneElement(child)}
+ ))}
+
+
);
}
diff --git a/src/h5web/toolbar/Toolbar.module.css b/src/h5web/toolbar/Toolbar.module.css
index 47776b052..751bba30e 100644
--- a/src/h5web/toolbar/Toolbar.module.css
+++ b/src/h5web/toolbar/Toolbar.module.css
@@ -56,6 +56,11 @@
font-size: 0.875em;
}
+.btn[data-raised] > .btnLike {
+ background-color: var(--btn-bg-raised);
+ box-shadow: 0 0 1px var(--btn-shadow-color);
+}
+
.icon {
font-size: 1.5em;
padding-top: 1px;
@@ -102,3 +107,16 @@
.btn[aria-expanded='true']:hover > .btnLike {
box-shadow: var(--btn-shadow-pressed) var(--btn-shadow-dark-color);
}
+
+.popup {
+ position: absolute;
+ bottom: 1px; /* guarantees overlap with toolbar for hover-only popup */
+ padding-top: calc(0.375rem + 1px);
+ transform: translateY(100%);
+}
+
+.popupInner {
+ background-color: var(--secondary-light-bg-90);
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 1px,
+ rgba(0, 0, 0, 0.1) 0px 4px 11px;
+}
diff --git a/src/h5web/toolbar/controls/DomainSlider.module.css b/src/h5web/toolbar/controls/DomainSlider.module.css
deleted file mode 100644
index 001b8f44f..000000000
--- a/src/h5web/toolbar/controls/DomainSlider.module.css
+++ /dev/null
@@ -1,50 +0,0 @@
-.sliderWrapper {
- display: flex;
- padding: 0 0 0 0.5rem;
-}
-
-.slider {
- display: flex;
- width: 8rem;
- margin-right: 0.25rem;
- font-size: 0.75rem;
- cursor: pointer;
-}
-
-.slider:global(.disabled) {
- opacity: 0.5;
- pointer-events: none;
-}
-
-.track {
- align-self: center;
- height: 0.375rem;
- background-color: var(--secondary-dark-15);
- box-shadow: var(--btn-shadow-pressed) var(--btn-shadow-color);
- border-radius: 0.25rem;
-}
-
-.thumb {
- display: flex;
- align-items: center;
- height: 100%;
-}
-
-.thumbBtnLike {
- composes: btnLike from '../Toolbar.module.css';
- min-width: 2rem;
- padding: 0.375rem;
- background-color: var(--secondary-lighter);
- box-shadow: var(--btn-shadow-idle) var(--btn-shadow-color);
-}
-
-.thumb:hover > .thumbBtnLike {
- box-shadow: var(--btn-shadow-idle) var(--btn-shadow-dark-color);
-}
-
-.autoBtnLike {
- composes: btnLike from '../Toolbar.module.css';
- font-size: 0.875em;
- border-radius: 0.875rem;
- color: var(--black);
-}
diff --git a/src/h5web/toolbar/controls/DomainSlider.tsx b/src/h5web/toolbar/controls/DomainSlider.tsx
deleted file mode 100644
index b5e1afa0d..000000000
--- a/src/h5web/toolbar/controls/DomainSlider.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import type { ReactElement } from 'react';
-import ReactSlider from 'react-slider';
-import { format } from 'd3-format';
-import styles from './DomainSlider.module.css';
-import type { CustomDomain, Domain } from '../../vis-packs/core/models';
-import { extendDomain } from '../../vis-packs/core/utils';
-import ToggleBtn from './ToggleBtn';
-import { FiZap } from 'react-icons/fi';
-
-const EXTEND_FACTOR = 0.2;
-const NB_DECIMALS = 1;
-
-interface Props {
- dataDomain: Domain;
- value: CustomDomain;
- disabled?: boolean;
- onChange: (value: CustomDomain) => void;
-}
-
-function DomainSlider(props: Props): ReactElement {
- const { dataDomain, value, disabled, onChange } = props;
-
- const [extendedMin, extendedMax] = extendDomain(dataDomain, EXTEND_FACTOR);
- const step = Math.max((extendedMax - extendedMin) / 100, 10 ** -NB_DECIMALS);
-
- const isAutoMin = value[0] === undefined;
- const isAutoMax = value[1] === undefined;
- const isAutoScaling = isAutoMin || isAutoMax;
-
- return (
-
-
(
-
-
- {format(`.${NB_DECIMALS}f`)(valueNow)}
-
-
- )}
- value={[value[0] ?? dataDomain[0], value[1] ?? dataDomain[1]]}
- onAfterChange={(bounds) => {
- onChange(bounds as Domain);
- }}
- min={extendedMin}
- max={extendedMax}
- step={step}
- pearling
- />
-
- {
- onChange(isAutoScaling ? dataDomain : [undefined, undefined]);
- }}
- disabled={disabled}
- />
-
- );
-}
-
-export default DomainSlider;
diff --git a/src/h5web/toolbar/controls/DomainSlider/DomainSlider.module.css b/src/h5web/toolbar/controls/DomainSlider/DomainSlider.module.css
new file mode 100644
index 000000000..0abe1721c
--- /dev/null
+++ b/src/h5web/toolbar/controls/DomainSlider/DomainSlider.module.css
@@ -0,0 +1,123 @@
+.root {
+ position: relative;
+ display: flex;
+ padding: 0 0.375rem;
+ margin-right: -0.375rem;
+}
+
+.sliderWrapper {
+ display: flex;
+}
+
+.slider {
+ display: flex;
+ width: 8rem;
+ margin-right: 0.25rem;
+ font-size: 0.75rem;
+ cursor: pointer;
+}
+
+.slider:global(.disabled) {
+ opacity: 0.5;
+ pointer-events: none;
+}
+
+.track {
+ align-self: center;
+ height: 0.375rem;
+ background-color: var(--secondary-dark-15);
+ box-shadow: var(--btn-shadow-pressed) var(--btn-shadow-color);
+ border-radius: 0.25rem;
+}
+
+.thumb {
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+
+.thumbBtnLike {
+ composes: btnLike from '../../Toolbar.module.css';
+ min-width: 2rem;
+ padding: 0.375rem;
+ background-color: var(--secondary-lighter);
+ box-shadow: var(--btn-shadow-idle) var(--btn-shadow-color);
+}
+
+.thumb:hover > .thumbBtnLike {
+ box-shadow: var(--btn-shadow-idle) var(--btn-shadow-dark-color);
+}
+
+.autoBtnLike {
+ composes: btnLike from '../../Toolbar.module.css';
+ font-size: 0.875em;
+ border-radius: 0.875rem;
+ color: var(--black);
+}
+
+.tooltip {
+ composes: popup from '../../Toolbar.module.css';
+ left: 50%;
+ min-width: 16em;
+ transform: translate(-50%, 100%);
+}
+
+.tooltipInner {
+ composes: popupInner from '../../Toolbar.module.css';
+ padding: 1rem 0.75rem;
+}
+
+.minMax {
+ display: grid;
+ grid-template-columns: [label] auto [value] 1fr [actions] 2.5em;
+ grid-gap: 0.5rem;
+ margin-bottom: 1rem;
+ font-weight: bold;
+ color: var(--near-black);
+}
+
+.minMax > h3,
+.minMax > p {
+ margin-bottom: 0;
+}
+
+.minMax > h3 {
+ grid-column: label;
+ text-transform: uppercase;
+ font-size: inherit;
+}
+
+.minMax > p {
+ grid-column: value;
+ display: flex;
+ align-items: center;
+ text-align: right;
+}
+
+.minMax > p::before {
+ flex: 1 1 0%;
+ content: '';
+ min-width: 1em;
+ margin-right: 0.5rem;
+ margin-top: 0.5rem;
+ border-bottom: 2px dotted currentColor;
+}
+
+.dataRange {
+ margin-bottom: 0.75rem;
+ white-space: nowrap;
+}
+
+.dataRange > span {
+ margin-left: 0.5rem;
+}
+
+.autoscale {
+ display: flex;
+ align-items: center;
+ margin-bottom: 0;
+}
+
+.autoscale > button:first-of-type {
+ margin-left: 0.5rem;
+}
diff --git a/src/h5web/toolbar/controls/DomainSlider/DomainSlider.tsx b/src/h5web/toolbar/controls/DomainSlider/DomainSlider.tsx
new file mode 100644
index 000000000..745a3f261
--- /dev/null
+++ b/src/h5web/toolbar/controls/DomainSlider/DomainSlider.tsx
@@ -0,0 +1,115 @@
+import type { ReactElement } from 'react';
+import ReactSlider from 'react-slider';
+import styles from './DomainSlider.module.css';
+import type { CustomDomain, Domain } from '../../../vis-packs/core/models';
+import { extendDomain } from '../../../vis-packs/core/utils';
+import ToggleBtn from '../ToggleBtn';
+import { FiZap } from 'react-icons/fi';
+import { useKey, useToggle } from 'react-use';
+import DomainTooltip from './DomainTooltip';
+import { format } from 'd3-format';
+
+const EXTEND_FACTOR = 0.2;
+const NB_DECIMALS = 1;
+const TOOLTIP_ID = 'domain-tooltip';
+
+const formatThumb = format(`.1f`);
+
+function getAutoscaleLabel(isAutoMin: boolean, isAutoMax: boolean): string {
+ if (isAutoMin && !isAutoMax) {
+ return 'Min';
+ }
+
+ if (isAutoMax && !isAutoMin) {
+ return 'Max';
+ }
+
+ return 'Auto';
+}
+
+interface Props {
+ dataDomain: Domain;
+ customDomain: CustomDomain;
+ disabled?: boolean;
+ onCustomDomainChange: (domain: CustomDomain) => void;
+}
+
+function DomainSlider(props: Props): ReactElement {
+ const { dataDomain, customDomain, disabled, onCustomDomainChange } = props;
+
+ const [tooltipOpen, toggleTooltip] = useToggle(false);
+ useKey('Escape', () => toggleTooltip(false));
+
+ const [extendedMin, extendedMax] = extendDomain(dataDomain, EXTEND_FACTOR);
+ const step = Math.max((extendedMax - extendedMin) / 100, 10 ** -NB_DECIMALS);
+
+ const isAutoMin = customDomain[0] === undefined;
+ const isAutoMax = customDomain[1] === undefined;
+ const isAutoscaling = isAutoMin || isAutoMax;
+
+ const appliedDomain: Domain = [
+ customDomain[0] ?? dataDomain[0],
+ customDomain[1] ?? dataDomain[1],
+ ];
+
+ return (
+ toggleTooltip(true)}
+ onPointerLeave={() => toggleTooltip(false)}
+ >
+
onCustomDomainChange(bounds as Domain)}
+ renderThumb={(thumbProps, { valueNow }) => (
+
+
{formatThumb(valueNow)}
+
+ )}
+ />
+
+ {
+ onCustomDomainChange(
+ isAutoscaling ? dataDomain : [undefined, undefined]
+ );
+ }}
+ />
+
+ {
+ const newMin = isAutoMin ? dataDomain[0] : undefined;
+ onCustomDomainChange([newMin, customDomain[1]]);
+ }}
+ onAutoMaxToggle={() => {
+ const newMax = isAutoMax ? dataDomain[1] : undefined;
+ onCustomDomainChange([customDomain[0], newMax]);
+ }}
+ />
+
+ );
+}
+
+export default DomainSlider;
diff --git a/src/h5web/toolbar/controls/DomainSlider/DomainTooltip.tsx b/src/h5web/toolbar/controls/DomainSlider/DomainTooltip.tsx
new file mode 100644
index 000000000..f4cffb449
--- /dev/null
+++ b/src/h5web/toolbar/controls/DomainSlider/DomainTooltip.tsx
@@ -0,0 +1,73 @@
+import { format } from 'd3-format';
+import type { ReactElement } from 'react';
+import type { Domain } from '../../../../packages/lib';
+import ToggleBtn from '../ToggleBtn';
+import styles from './DomainSlider.module.css';
+
+const formatter = format('.3~e');
+
+interface Props {
+ id: string;
+ open: boolean;
+ domain: Domain;
+ dataDomain: Domain;
+ isAutoMin: boolean;
+ isAutoMax: boolean;
+ onAutoMinToggle: () => void;
+ onAutoMaxToggle: () => void;
+}
+
+function DomainTooltip(props: Props): ReactElement {
+ const { id, open, domain, dataDomain, isAutoMin, isAutoMax } = props;
+ const { onAutoMinToggle, onAutoMaxToggle } = props;
+
+ return (
+
+
+
+
Min
+
+ {formatter(domain[0])}
+
+
Max
+
+ {formatter(domain[1])}
+
+
+
+
+ Data range{' '}
+
+ [{' '}
+
+ {formatter(dataDomain[0])}
+ {' '}
+ ,{' '}
+
+ {formatter(dataDomain[1])}
+ {' '}
+ ]
+
+
+
+
+ Autoscale{' '}
+
+
+
+
+
+ );
+}
+
+export default DomainTooltip;
diff --git a/src/h5web/toolbar/controls/ToggleBtn.tsx b/src/h5web/toolbar/controls/ToggleBtn.tsx
index b9e7b6759..51909fb44 100644
--- a/src/h5web/toolbar/controls/ToggleBtn.tsx
+++ b/src/h5web/toolbar/controls/ToggleBtn.tsx
@@ -7,13 +7,14 @@ interface Props {
icon?: IconType;
iconOnly?: boolean;
small?: boolean;
+ raised?: boolean;
value: boolean;
onChange: () => void;
disabled?: boolean;
}
function ToggleBtn(props: Props): ReactElement {
- const { label, small, value, onChange, disabled } = props;
+ const { label, small, raised, value, onChange, disabled } = props;
const { icon: Icon, iconOnly } = props;
return (
@@ -25,6 +26,7 @@ function ToggleBtn(props: Props): ReactElement {
onClick={onChange}
disabled={disabled}
data-small={small || undefined}
+ data-raised={raised || undefined}
>
{Icon && }
diff --git a/src/h5web/vis-packs/core/heatmap/ColorBar.module.css b/src/h5web/vis-packs/core/heatmap/ColorBar.module.css
index 635f81036..b6452e2b6 100644
--- a/src/h5web/vis-packs/core/heatmap/ColorBar.module.css
+++ b/src/h5web/vis-packs/core/heatmap/ColorBar.module.css
@@ -51,6 +51,7 @@
justify-content: center;
line-height: 1;
font-size: 0.875em;
+ white-space: nowrap;
}
.minBound {
diff --git a/src/h5web/vis-packs/core/heatmap/ColorBar.tsx b/src/h5web/vis-packs/core/heatmap/ColorBar.tsx
index 468df8eca..4b8390f7b 100644
--- a/src/h5web/vis-packs/core/heatmap/ColorBar.tsx
+++ b/src/h5web/vis-packs/core/heatmap/ColorBar.tsx
@@ -8,7 +8,7 @@ import type { ScaleType, Domain } from '../models';
import type { ColorMap } from './models';
import { format } from 'd3-format';
-const boundFormatter = format('.3~g');
+const boundFormatter = format('.3~e');
interface Props {
domain: Domain;
diff --git a/src/styles/base.css b/src/styles/base.css
index 668e399f9..bcc9818c3 100644
--- a/src/styles/base.css
+++ b/src/styles/base.css
@@ -43,6 +43,12 @@ pre {
font-family: var(--monospace);
}
+abbr[title] {
+ cursor: help;
+ border-bottom: 1px dotted;
+ text-decoration: none;
+}
+
[hidden] {
display: none !important;
}
diff --git a/src/styles/vars.css b/src/styles/vars.css
index e5cd42ca7..c7ad8bd7c 100644
--- a/src/styles/vars.css
+++ b/src/styles/vars.css
@@ -1,9 +1,10 @@
:root {
--black: #020402;
+ --near-black: #333;
--dark-slate-gray: #2f4f4f;
--dark-gray: #696969;
--gray: #808080;
- --white: #ffffff;
+ --white: #fff;
--primary: #c0da74;
--primary-light: #d4e09b;
--primary-dark: #9aae5d;
@@ -19,6 +20,7 @@
--secondary-darker: #0e5846;
--secondary-bg: #d9f4ec;
--secondary-light-bg: #ecfaf6;
+ --secondary-light-bg-90: #ebfaf5e6;
--danger: #99231b;
--monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier,
monospace;
@@ -30,4 +32,5 @@
--btn-shadow-color: var(--gray);
--btn-shadow-dark-color: var(--dark-gray);
--btn-shadow-darker-color: var(--dark-slate-gray);
+ --btn-bg-raised: var(--white);
}