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 { - + + ); } 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 ( + + ); +} + +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); }