Skip to content

Date filter #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9c30a93
feat: add date filter types and context interfaces
indraneel Jul 9, 2025
7cc502e
feat: add date filter utilities and URL persistence
indraneel Jul 9, 2025
acb9bba
feat: implement date filter state management in provider
indraneel Jul 9, 2025
da5663f
feat: add filtered data hooks for date filtering
indraneel Jul 9, 2025
d923853
feat: integrate date filtering with search and GeoParquet
indraneel Jul 9, 2025
f699827
feat: add date filter UI components and integration
indraneel Jul 9, 2025
1a4dad6
feat: integrate date filtering with map component
indraneel Jul 9, 2025
e254346
add time filtering
indraneel Jul 9, 2025
2f5759d
Update date-filter.ts
indraneel Jul 9, 2025
8f9a9b7
fix: prettier
indraneel Jul 9, 2025
3d071ee
make datetime filter collapsible
indraneel Jul 10, 2025
4203499
move date filter types
indraneel Jul 10, 2025
e583a45
add logic to reset date range when the STAC value changes, respecting…
indraneel Jul 10, 2025
1de5911
fix date boundary check logic
indraneel Jul 10, 2025
32db869
lint fix
indraneel Jul 10, 2025
1b85277
lint fix
indraneel Jul 10, 2025
46ba6a5
feat: implement dual date filtering system
indraneel Jul 11, 2025
9ef7977
reorganize filters
indraneel Jul 11, 2025
2a40e38
feat: add sliding date filter
indraneel Jul 11, 2025
576da18
add zoom to scrubber
indraneel Jul 11, 2025
4d86a3c
fix: prettier
indraneel Jul 11, 2025
c15dd28
Update stac-geoparquet.ts
indraneel Jul 11, 2025
a935fe5
add some defensive checking to slider
indraneel Jul 11, 2025
df30e20
Update item.tsx
indraneel Jul 11, 2025
8eaa295
Merge branch 'fix-layout' into date-filter-new
indraneel Jul 11, 2025
e5a2774
lint/prettier fix
indraneel Jul 11, 2025
7173605
fix: add debounce to stop history from thrashing
indraneel Jul 14, 2025
ecceb16
fix: resolve slider validation error when clicking "this year"
indraneel Jul 14, 2025
8c125cc
fix: html button nesting error
indraneel Jul 14, 2025
3de3085
feat: keep track of temporal data and disable tab accordingly
indraneel Jul 14, 2025
7ca9037
feat: show full range for client side slider
indraneel Jul 14, 2025
49106f6
fix: prettier
indraneel Jul 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions src/components/date-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {
Alert,
Button,
ButtonGroup,
HStack,
Input,
Text,
VStack,
} from "@chakra-ui/react";
import { LuCalendar, LuX } from "react-icons/lu";
import { DATE_FILTER_PRESETS, isValidDateRange } from "../utils/date-filter";

export interface DateRange {
startDate: Date | null;
endDate: Date | null;
startTime?: string; // HH:mm format
endTime?: string; // HH:mm format
}

export interface DateFilterPreset {
id: string;
label: string;
getDateRange: () => DateRange;
}

interface DateFilterProps {
dateRange: DateRange;
setDateRange: (dateRange: DateRange) => void;
clearDateRange: () => void;
isDateFilterActive: boolean;
title?: string;
description?: string;
}

export default function DateFilter({
dateRange,
setDateRange,
clearDateRange,
isDateFilterActive,
title = "Date & Time Filter",
description,
}: DateFilterProps) {
const handlePresetSelect = (preset: (typeof DATE_FILTER_PRESETS)[0]) => {
setDateRange(preset.getDateRange());
};

const handleCustomDateChange = (
field: keyof DateRange,
value: Date | string | null,
) => {
setDateRange({ ...dateRange, [field]: value });
};

const handleTimeChange = (field: "startTime" | "endTime", value: string) => {
setDateRange({ ...dateRange, [field]: value || undefined });
};

return (
<VStack
gap={4}
align="stretch"
p={4}
borderWidth={1}
borderRadius="md"
bg="white"
shadow="sm"
>
<HStack justify="space-between" align="center">
<HStack>
<LuCalendar />
<Text fontSize="sm" fontWeight="medium">
{title}
</Text>
</HStack>
{isDateFilterActive && (
<Button
size="sm"
variant="ghost"
colorScheme="red"
onClick={clearDateRange}
>
<LuX /> Clear
</Button>
)}
</HStack>

{description && (
<Text fontSize="xs" color="gray.600">
{description}
</Text>
)}

<VStack gap={3} align="stretch">
<Text fontSize="sm" fontWeight="medium">
Quick Presets
</Text>
<ButtonGroup size="sm" variant="outline" flexWrap="wrap">
{DATE_FILTER_PRESETS.map((preset) => (
<Button key={preset.id} onClick={() => handlePresetSelect(preset)}>
{preset.label}
</Button>
))}
</ButtonGroup>

<Text fontSize="sm" fontWeight="medium">
Custom Range
</Text>
<VStack gap={3} align="stretch">
{/* Start Date and Time */}
<VStack align="stretch" gap={2}>
<Text fontSize="xs" fontWeight="medium">
Start Date & Time
</Text>
<HStack gap={2}>
<Input
type="date"
size="sm"
flex={1}
value={dateRange.startDate?.toISOString().split("T")[0] || ""}
onChange={(e) =>
handleCustomDateChange(
"startDate",
e.target.value ? new Date(e.target.value) : null,
)
}
/>
<Input
type="time"
size="sm"
flex={1}
value={dateRange.startTime || ""}
onChange={(e) => handleTimeChange("startTime", e.target.value)}
/>
</HStack>
</VStack>

<VStack align="stretch" gap={2}>
<Text fontSize="xs" fontWeight="medium">
End Date & Time
</Text>
<HStack gap={2}>
<Input
type="date"
size="sm"
flex={1}
value={dateRange.endDate?.toISOString().split("T")[0] || ""}
onChange={(e) =>
handleCustomDateChange(
"endDate",
e.target.value ? new Date(e.target.value) : null,
)
}
/>
<Input
type="time"
size="sm"
flex={1}
value={dateRange.endTime || ""}
onChange={(e) => handleTimeChange("endTime", e.target.value)}
/>
</HStack>
</VStack>
</VStack>

{!isValidDateRange(dateRange) && (
<Alert.Root status="error">
<Alert.Indicator />
<Alert.Content>
<Alert.Description>
Start date/time must be before or equal to end date/time
</Alert.Description>
</Alert.Content>
</Alert.Root>
)}

{isDateFilterActive && (
<Text fontSize="xs" color="green.600" fontWeight="medium">
✓ Date & time filter is active
</Text>
)}
</VStack>
</VStack>
);
}
12 changes: 12 additions & 0 deletions src/components/filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import SlidingDateFilter from "./sliding-date-filter";

export default function Filter() {
return (
<>
<SlidingDateFilter
title="Temporal Scrubber"
description="Scrub through the temporal range of loaded data"
/>
</>
);
}
26 changes: 17 additions & 9 deletions src/components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
} from "react-map-gl/maplibre";
import type { StacCollection } from "stac-ts";
import useStacMap from "../hooks/stac-map";
import {
useFilteredSearchItems,
useFilteredCollections,
} from "../hooks/stac-filtered-data";
import { useColorModeValue } from "./ui/color-mode";

function DeckGLOverlay(props: DeckProps) {
Expand Down Expand Up @@ -46,15 +50,17 @@ export default function Map() {
);
const {
value,
collections,
searchItems,
stacGeoparquetTable,
stacGeoparquetMetadata,
stacGeoparquetItem,
setStacGeoparquetItemId,
setPicked,
picked,
} = useStacMap();

const filteredSearchItems = useFilteredSearchItems();
const filteredCollections = useFilteredCollections();

const [data, setData] = useState<GeoJSON | Feature[]>();
const [visible, setVisible] = useState(true);
const [filled, setFilled] = useState(true);
Expand All @@ -66,10 +72,12 @@ export default function Map() {
useEffect(() => {
switch (value?.type) {
case "Catalog":
setBbbox(collections && getCollectionsExtent(collections));
setBbbox(
filteredCollections && getCollectionsExtent(filteredCollections),
);
setData(
collections &&
collections.map((collection) =>
filteredCollections &&
filteredCollections.map((collection) =>
bboxPolygon(collection.extent?.spatial?.bbox?.[0] as BBox),
),
);
Expand Down Expand Up @@ -97,7 +105,7 @@ export default function Map() {
setFilled(true);
setPickable(true);
}
}, [value, collections]);
}, [value, filteredCollections]);

useEffect(() => {
if (stacGeoparquetTable) {
Expand Down Expand Up @@ -161,14 +169,14 @@ export default function Map() {
}, [bbox]);

useEffect(() => {
if (searchItems.length > 0) {
if (filteredSearchItems.length > 0) {
setVisible(false);
} else {
setVisible(true);
}
}, [searchItems]);
}, [filteredSearchItems]);

const searchLayers = searchItems.map((items, index) => {
const searchLayers = filteredSearchItems.map((items, index) => {
return new GeoJsonLayer({
id: `search-${index}`,
data: items as Feature[],
Expand Down
55 changes: 48 additions & 7 deletions src/components/panel.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import { SkeletonText, Tabs } from "@chakra-ui/react";
import { SkeletonText, Tabs, Accordion, HStack, Text } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import {
LuInfo,
LuMousePointerClick,
LuSearch,
LuUpload,
LuFilter,
} from "react-icons/lu";
import type { StacLink } from "stac-ts";
import useStacMap from "../hooks/stac-map";
import useStacValue from "../hooks/stac-value";
import ItemSearch from "./search/item";
import Upload from "./upload";
import Value from "./value";
import Filter from "./filter";

export default function Panel() {
const { value, picked } = useStacMap();
const {
value,
picked,
dateRange,
setDateRange,
clearDateRange,
isDateFilterActive,
hasTemporalData,
} = useStacMap();
const [tab, setTab] = useState<string>("upload");
const [itemSearchLinks, setItemSearchLinks] = useState<StacLink[]>([]);
const { value: root } = useStacValue(
Expand Down Expand Up @@ -62,6 +72,9 @@ export default function Panel() {
>
<LuSearch></LuSearch>
</Tabs.Trigger>
<Tabs.Trigger value="filter" disabled={!hasTemporalData}>
<LuFilter></LuFilter>
</Tabs.Trigger>
<Tabs.Trigger value="picked" disabled={!picked}>
<LuMousePointerClick></LuMousePointerClick>
</Tabs.Trigger>
Expand All @@ -77,13 +90,41 @@ export default function Panel() {
</Tabs.Content>
<Tabs.Content value="search">
{value && itemSearchLinks.length > 0 && (
<ItemSearch
value={value}
links={itemSearchLinks}
defaultLink={itemSearchLinks[0]}
></ItemSearch>
<Accordion.Root
variant="outline"
size="sm"
collapsible
defaultValue={["item-search"]}
>
<Accordion.Item value="item-search">
<Accordion.ItemTrigger>
<HStack justify="space-between" width="100%">
<Text fontSize="sm" fontWeight="medium">
Item Search
</Text>
<Accordion.ItemIndicator />
</HStack>
</Accordion.ItemTrigger>
<Accordion.ItemContent>
<Accordion.ItemBody>
<ItemSearch
value={value}
links={itemSearchLinks}
defaultLink={itemSearchLinks[0]}
dateRange={dateRange}
setDateRange={setDateRange}
clearDateRange={clearDateRange}
isDateFilterActive={isDateFilterActive}
/>
</Accordion.ItemBody>
</Accordion.ItemContent>
</Accordion.Item>
</Accordion.Root>
)}
</Tabs.Content>
<Tabs.Content value="filter">
<Filter />
</Tabs.Content>
<Tabs.Content value="picked">
{picked && <Value value={picked}></Value>}
</Tabs.Content>
Expand Down
Loading