Skip to content

Commit

Permalink
Add date range selector histogram
Browse files Browse the repository at this point in the history
  • Loading branch information
lmcnulty committed May 31, 2024
1 parent a5ddcaa commit 45cd3d1
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 17 deletions.
4 changes: 2 additions & 2 deletions site/gatsby-site/src/components/discover/Controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Trans } from 'react-i18next';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';

const Controls = () => {
const Controls = ({ bins }) => {
const { indexUiState } = useInstantSearch();

const [expandFilters, setExpandFilters] = useState(false);
Expand Down Expand Up @@ -75,7 +75,7 @@ const Controls = () => {

<div className="basis-full order-3" />

<Filters {...{ expandFilters }} />
<Filters {...{ expandFilters, bins }} />
</div>
);
};
Expand Down
4 changes: 2 additions & 2 deletions site/gatsby-site/src/components/discover/Discover.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function mapping() {
};
}

export default function Discover() {
export default function Discover({ bins }) {
const { locale } = useLocalization();

const [indexName] = useState(`instant_search-${locale}-featured`);
Expand Down Expand Up @@ -114,7 +114,7 @@ export default function Discover() {
</Col>
</Row>

{width > 767 ? <Controls /> : <OptionsModal />}
{width > 767 ? <Controls {...{ bins }} /> : <OptionsModal />}

<Hits />

Expand Down
15 changes: 8 additions & 7 deletions site/gatsby-site/src/components/discover/Filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function ToggleContent({ label, touched, faIcon, toggled, accordion = false }) {
);
}

function ButtonToggle({ label, faIcon, touched, type, filterProps }) {
function ButtonToggle({ label, faIcon, touched, type, filterProps, bins }) {
const [toggled, setToggled] = useState(true);

const toggleDropdown = () => {
Expand Down Expand Up @@ -105,20 +105,20 @@ function ButtonToggle({ label, faIcon, touched, type, filterProps }) {
toggled ? 'hidden' : 'block '
} bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700`}
>
<FilterOverlay type={type} filterProps={filterProps} />
<FilterOverlay type={type} filterProps={filterProps} {...{ bins }} />
</div>
</div>
);
}

function FilterContent({ type, filterProps }) {
function FilterContent({ type, filterProps, bins }) {
const { default: Component } = componentsMap[type];

return <Component {...filterProps} />;
return <Component {...filterProps} {...{ bins }} />;
}

const FilterOverlay = React.forwardRef(function Container(
{ type, filterProps, ...overlayProps },
{ bins, type, filterProps, ...overlayProps },
ref
) {
return (
Expand All @@ -131,14 +131,14 @@ const FilterOverlay = React.forwardRef(function Container(
>
<Card className="shadow-lg card">
<div>
<FilterContent type={type} filterProps={filterProps} />
<FilterContent type={type} filterProps={filterProps} {...{ bins }} />
</div>
</Card>
</div>
);
});

export default function Filter({ type, ...filterProps }) {
export default function Filter({ type, bins, ...filterProps }) {
const { label, faIcon, attribute } = filterProps;

const { touchedCount } = componentsMap[type];
Expand All @@ -155,6 +155,7 @@ export default function Filter({ type, ...filterProps }) {
touched={touched}
type={type}
filterProps={filterProps}
{...{ bins }}
/>
</>
);
Expand Down
4 changes: 2 additions & 2 deletions site/gatsby-site/src/components/discover/Filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import REFINEMENT_LISTS from 'components/discover/REFINEMENT_LISTS';
import Filter from './Filter';
import { graphql, useStaticQuery } from 'gatsby';

function Filters({ expandFilters }) {
function Filters({ expandFilters, bins }) {
const {
taxa: { nodes: taxa },
} = useStaticQuery(graphql`
Expand Down Expand Up @@ -44,7 +44,7 @@ function Filters({ expandFilters }) {
}
return (
<div key={list.attribute} className={className} data-cy={list.attribute}>
<Filter className="w-full" type={list.type} taxa={taxa} {...list} />
<Filter className="w-full" type={list.type} taxa={taxa} {...list} {...{ bins }} />
</div>
);
})}
Expand Down
208 changes: 206 additions & 2 deletions site/gatsby-site/src/components/discover/filterTypes/RangeInput.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { useRange } from 'react-instantsearch';
import { Trans } from 'react-i18next';
import { debounce } from 'debounce';
Expand All @@ -13,7 +13,7 @@ const formatDate = (epoch) => new Date(epoch * 1000).toISOString().substr(0, 10)

const dateToEpoch = (date) => new Date(date).getTime() / 1000;

export default function RangeInput({ attribute }) {
export default function RangeInput({ attribute, bins }) {
const {
range: { min, max },
start,
Expand Down Expand Up @@ -53,6 +53,18 @@ export default function RangeInput({ attribute }) {
{({ values, errors, touched, handleBlur }) => (
<>
<Form className="px-3">
<DoubleRangeSlider
globalMin={min}
globalMax={max}
selectionMax={currentRefinement.max}
setSelectionMin={debounce((min) => {
onChange({ min, max: currentRefinement.max });

Check warning on line 61 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L60-L61

Added lines #L60 - L61 were not covered by tests
})}
setSelectionMax={debounce((max) => {
onChange({ max, min: currentRefinement.min });
}, 2000)}
{...{ bins }}
/>
<FieldContainer>
<Label>
<Trans>From Date</Trans>:
Expand Down Expand Up @@ -135,6 +147,198 @@ export default function RangeInput({ attribute }) {
);
}

const proportionToPercent = (proportion) => 100 * proportion + '%';

const sliderHeight = '1rem';

const DoubleRangeSlider = ({
globalMin,
globalMax,
selectionMax,
setSelectionMin,
setSelectionMax,
bins,
}) => {
const [lowerBound, bareSetLowerBound] = useState();

const setLowerBound = (value) => {
bareSetLowerBound(value);
setSelectionMin(globalMin + (globalMax - globalMin) * value);

Check warning on line 166 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L165-L166

Added lines #L165 - L166 were not covered by tests
};

const [upperBound, bareSetUpperBound] = useState();

const setUpperBound = (value) => {
bareSetUpperBound(value);
setSelectionMax(globalMin + (globalMax - globalMin) * value);
};

const binsMax = Math.max(...bins);

useEffect(() => {
setUpperBound((selectionMax - globalMin) / (globalMax - globalMin));
}, [selectionMax, globalMax, globalMin]);

return (
<>
<div className="h-32 w-full bg-gray-100 relative flex items-end">
{bins.map((bin, i) => (
<div
key={i}
className={`border-1 border-white ${
(i + 1) / bins.length >= lowerBound && i / bins.length <= upperBound
? 'bg-blue-500'

Check warning on line 190 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L190

Added line #L190 was not covered by tests
: 'bg-gray-700'
}`}
style={{
width: proportionToPercent(1 / bins.length),
height: proportionToPercent(Math.log(bin) / Math.log(binsMax * 0.9)),
}}
/>
))}
</div>
<div className="relative bg-gray-200" style={{ height: sliderHeight }}>
<div
className="bg-blue-600 h-full absolute"
style={{
left: `calc(${proportionToPercent(lowerBound)})`,
width: proportionToPercent(upperBound - lowerBound),
}}
/>

<SliderKnob bound={lowerBound} setBound={setLowerBound} ceiling={upperBound} />
<SliderKnob bound={upperBound} setBound={setUpperBound} floor={lowerBound} />
</div>
</>
);
};

const SliderKnob = ({ bound, setBound, ceiling = 1, floor = 0 }) => {
const [dragOffset, setDragOffset] = useState(null);

const [handlePosition, setHandlePosition] = useState({
top: '0px',
left: proportionToPercent(bound),
});

const knobRef = useRef();

useEffect(() => {
if (dragOffset) {
knobRef.current.focus();

Check warning on line 228 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L228

Added line #L228 was not covered by tests
}
});

useEffect(() => {
setHandlePosition({
top: '0px',
left: proportionToPercent(bound),
});
}, [bound]);

const updatePosition = (event) => {
if (dragOffset) {
const parentClientRect = event.target.parentNode.getBoundingClientRect();

Check warning on line 241 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L241

Added line #L241 was not covered by tests

const handleLeftPx = event.clientX - parentClientRect.x - dragOffset.x;

Check warning on line 243 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L243

Added line #L243 was not covered by tests

setHandlePosition({

Check warning on line 245 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L245

Added line #L245 was not covered by tests
top: event.clientY - parentClientRect.y - dragOffset.y + 'px',
left: handleLeftPx + 'px',
});

const handleLeftPercent = handleLeftPx / parentClientRect.width;

Check warning on line 250 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L250

Added line #L250 was not covered by tests

const newBound = clamp(handleLeftPercent, { floor, ceiling });

Check warning on line 252 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L252

Added line #L252 was not covered by tests

setBound(newBound);

Check warning on line 254 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L254

Added line #L254 was not covered by tests

return newBound;

Check warning on line 256 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L256

Added line #L256 was not covered by tests
}
};

return (
<>
<button
ref={knobRef}
className="rounded-full bg-white absolute focus:border-2 border-blue-500"
style={{
height: sliderHeight,
width: sliderHeight,
left: `calc(${proportionToPercent(bound)} - calc(${sliderHeight} / 2))`,
}}
onKeyDown={(event) => {

Check warning on line 270 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L270

Added line #L270 was not covered by tests
if (event.key == 'ArrowLeft') {
const newBound = clamp(bound - 0.1, { floor, ceiling });

Check warning on line 272 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L272

Added line #L272 was not covered by tests

setBound(newBound);
setDragOffset(null);
setHandlePosition({

Check warning on line 276 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L274-L276

Added lines #L274 - L276 were not covered by tests
top: '0px',
left: proportionToPercent(newBound),
});
}
if (event.key == 'ArrowRight') {
const newBound = clamp(bound + 0.1, { floor, ceiling });

Check warning on line 282 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L282

Added line #L282 was not covered by tests

setBound(newBound);
setDragOffset(null);
setHandlePosition({

Check warning on line 286 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L284-L286

Added lines #L284 - L286 were not covered by tests
top: '0px',
left: proportionToPercent(newBound),
});
}
}}
/>

{/* This "handle" element moves with the cursor
* so that we track the mouse position
* while the knob is being dragged.
* Then we move the knob to match the handle in the x-axis.
*/}
<button
tabIndex="-1"
className="rounded-full absolute x-10"
onClick={() => {
knobRef.current.focus();

Check warning on line 303 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L302-L303

Added lines #L302 - L303 were not covered by tests
}}
onMouseDown={(event) => {
const clientRect = event.target.getBoundingClientRect();

Check warning on line 306 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L305-L306

Added lines #L305 - L306 were not covered by tests

const offset = { x: event.clientX - clientRect.x, y: event.clientY - clientRect.y };

Check warning on line 308 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L308

Added line #L308 was not covered by tests

setDragOffset(offset);

Check warning on line 310 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L310

Added line #L310 was not covered by tests
}}
onMouseUp={() => {
setDragOffset(null);
setHandlePosition({

Check warning on line 314 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L312-L314

Added lines #L312 - L314 were not covered by tests
// TODO: It would be more efficient to use translate()
// so as to trigger fewer re-layouts.
top: '0px',
left: proportionToPercent(bound),
});
}}
onMouseMove={(event) => {
updatePosition(event);

Check warning on line 322 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L321-L322

Added lines #L321 - L322 were not covered by tests
}}
style={{
height: sliderHeight,
width: sliderHeight,
...handlePosition,
...(dragOffset
? // We enlarge the handle during movement
// so that moving the cursor real fast doesn't take it outside the div
// before an update occurs.
{ scale: '100', transformOrigin: 'center' }

Check warning on line 332 in site/gatsby-site/src/components/discover/filterTypes/RangeInput.js

View check run for this annotation

Codecov / codecov/patch

site/gatsby-site/src/components/discover/filterTypes/RangeInput.js#L332

Added line #L332 was not covered by tests
: { left: `calc(${handlePosition.left} - calc(${sliderHeight} / 2))` }),
}}
/>
</>
);
};

const clamp = (value, { floor, ceiling }) => Math.max(Math.min(value, ceiling), floor);

export const touchedCount = ({ searchState, attribute }) =>
searchState?.range?.[attribute]?.split(':')[0] || searchState?.range?.[attribute]?.split(':')[1]
? 1
Expand Down
39 changes: 37 additions & 2 deletions site/gatsby-site/src/pages/apps/discover.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,35 @@ import React from 'react';
import HeadContent from 'components/HeadContent';
import Discover from 'components/discover/Discover';
import { useTranslation } from 'react-i18next';
import { graphql } from 'gatsby';

function DiscoverApp(props) {
const { data } = props;

const reports = data.reports?.nodes;

const numBins = 16;

const bins = new Array(numBins).fill().map(() => 0);

const publishDates = reports.map((report) => new Date(report.date_published));

const latestPublishDate = Math.max(...publishDates);

const earliestPublishDate = Math.min(...publishDates);

for (const publishDate of publishDates) {
const position =
(publishDate - earliestPublishDate) / (latestPublishDate - earliestPublishDate);

const index = Math.floor(position * (bins.length - 1));

bins[index] += 1;
}

function DiscoverApp() {
return (
<div className="w-full">
<Discover />
<Discover {...{ bins }} />
</div>
);
}
Expand All @@ -29,4 +53,15 @@ export const Head = (props) => {
);
};

export const query = graphql`
query DiscoverPageQuery {
reports: allMongodbAiidprodReports {
nodes {
report_number
date_published
}
}
}
`;

export default DiscoverApp;

0 comments on commit 45cd3d1

Please sign in to comment.