Skip to content

Commit 4d1f221

Browse files
committed
feat: Transition to dynamic GDELT data loading from remote S3/CDN parquet files and update autocomplete to query these sources.
1 parent 94f4e94 commit 4d1f221

9 files changed

Lines changed: 384 additions & 298 deletions

File tree

‎public/final_enriched.parquet‎

-1.9 MB
Binary file not shown.

‎src/components/TimeRangeSlider.tsx‎

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ export function TimeRangeSlider() {
2525
const { t } = useTranslation();
2626
const { language } = useLanguage();
2727
const { minDate, maxDate, loading: dateRangeLoading } = useDateRange();
28-
28+
2929
// Default to collapsed on mobile (screen width < 640px)
30-
// We initialize state lazily to avoid hydration mismatch if we were doing SSR,
30+
// We initialize state lazily to avoid hydration mismatch if we were doing SSR,
3131
// but since this is a SPA, simple matching is fine, explicitly controlled by user mainly.
3232
const [isCollapsed, setIsCollapsed] = useState(false);
3333

@@ -41,42 +41,35 @@ export function TimeRangeSlider() {
4141
sqlDateToDayIndex(filters.timeRange.end, effectiveMinDate),
4242
]);
4343

44-
// Sync effect: When real data range loads, update filters if they are currently default or invalid
44+
// Sync slider position when the data range or filter range changes
45+
// (e.g. catalog loads → AppState sets latest-day default → slider should position correctly)
4546
useEffect(() => {
4647
if (!dateRangeLoading && minDate && maxDate) {
47-
const DEFAULT_START = 20250119;
48-
const DEFAULT_END = 20260127;
49-
50-
const isDefault = filters.timeRange.start === DEFAULT_START && filters.timeRange.end === DEFAULT_END;
51-
const isOutOfBounds = filters.timeRange.start < minDate || filters.timeRange.end > maxDate;
52-
const isReversed = filters.timeRange.start > filters.timeRange.end;
53-
54-
if (isDefault || isOutOfBounds || isReversed) {
55-
console.log('Syncing time range to data:', minDate, maxDate);
56-
updateTimeRange({ start: minDate, end: maxDate });
57-
setLocalRange([0, sqlDateToDayIndex(maxDate, minDate)]);
58-
} else {
59-
// Even if we don't change the filter, we MUST re-calculate localRange
60-
// because effectiveMinDate has changed from fallback to real minDate.
61-
// The current localRange (day indices) was calculated against the fallback date.
62-
setLocalRange([
63-
sqlDateToDayIndex(filters.timeRange.start, minDate),
64-
sqlDateToDayIndex(filters.timeRange.end, minDate)
65-
]);
66-
}
48+
// Clamp filter range into the catalog range to handle the placeholder → real transition
49+
const clampedStart = Math.max(filters.timeRange.start, minDate);
50+
const clampedEnd = Math.min(filters.timeRange.end, maxDate);
51+
setLocalRange([
52+
sqlDateToDayIndex(clampedStart, minDate),
53+
sqlDateToDayIndex(clampedEnd, minDate),
54+
]);
6755
}
68-
}, [minDate, maxDate, dateRangeLoading, updateTimeRange]); // Removed filters.timeRange to avoid loops, explicit checks inside
56+
}, [minDate, maxDate, dateRangeLoading, filters.timeRange.start, filters.timeRange.end]);
6957

7058
// Debounce: only update filters after user stops sliding
7159
useEffect(() => {
7260
const timer = setTimeout(() => {
7361
// Guard against NaN
74-
if (!localRange || typeof localRange[0] !== 'number' || typeof localRange[1] !== 'number') return;
62+
if (!localRange || typeof localRange[0] !== 'number' || typeof localRange[1] !== 'number')
63+
return;
7564

7665
const startSQL = dayIndexToSQLDate(localRange[0], effectiveMinDate);
7766
const endSQL = dayIndexToSQLDate(localRange[1], effectiveMinDate);
7867

79-
if (startSQL && endSQL && (startSQL !== filters.timeRange.start || endSQL !== filters.timeRange.end)) {
68+
if (
69+
startSQL &&
70+
endSQL &&
71+
(startSQL !== filters.timeRange.start || endSQL !== filters.timeRange.end)
72+
) {
8073
updateTimeRange({ start: startSQL, end: endSQL });
8174
}
8275
}, 300);

‎src/context/app-state-context.tsx‎

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1-
import { createContext, useContext, useState, ReactNode, useCallback } from 'react';
1+
import {
2+
createContext,
3+
useContext,
4+
useState,
5+
ReactNode,
6+
useCallback,
7+
useEffect,
8+
useRef,
9+
} from 'react';
210
import type { BBox, TimeRange, FilterState } from '@/types/gdelt';
11+
import { useDuckDB } from '@/context/duckdb-context';
312

413
interface AppStateContextValue {
514
filters: FilterState;
15+
filtersReady: boolean;
616
selectedEventId: string | null;
717
updateTimeRange: (range: TimeRange) => void;
818
updateBBox: (bbox: BBox | null) => void;
@@ -18,14 +28,14 @@ interface AppStateContextValue {
1828

1929
const AppStateContext = createContext<AppStateContextValue | null>(null);
2030

21-
// Default date range: all data from dataset
22-
const DEFAULT_TIME_RANGE: TimeRange = {
23-
start: 20250119, // Jan 19, 2025
24-
end: 20260127, // Jan 27, 2026
31+
// Placeholder range used before catalog loads — intentionally broad
32+
const INITIAL_TIME_RANGE: TimeRange = {
33+
start: 20250101,
34+
end: 20261231,
2535
};
2636

2737
const DEFAULT_FILTERS: FilterState = {
28-
timeRange: DEFAULT_TIME_RANGE,
38+
timeRange: INITIAL_TIME_RANGE,
2939
bbox: null,
3040
sentiment: ['positive', 'neutral', 'negative'],
3141
eventTypes: [1, 2, 3, 4], // All quad classes
@@ -34,8 +44,23 @@ const DEFAULT_FILTERS: FilterState = {
3444
};
3545

3646
export function AppStateProvider({ children }: { children: ReactNode }) {
47+
const { fileCatalog } = useDuckDB();
3748
const [filters, setFilters] = useState<FilterState>(DEFAULT_FILTERS);
3849
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
50+
const [filtersReady, setFiltersReady] = useState(false);
51+
52+
// Stores the latest-day range for clearFilters
53+
const defaultRangeRef = useRef<TimeRange>(INITIAL_TIME_RANGE);
54+
55+
// When catalog arrives, default to the LATEST DAY only (1 file, fast)
56+
useEffect(() => {
57+
if (fileCatalog && !filtersReady) {
58+
const latestDay: TimeRange = { start: fileCatalog.maxDate, end: fileCatalog.maxDate };
59+
defaultRangeRef.current = latestDay;
60+
setFilters((prev) => ({ ...prev, timeRange: latestDay }));
61+
setFiltersReady(true);
62+
}
63+
}, [fileCatalog, filtersReady]);
3964

4065
const updateTimeRange = useCallback((range: TimeRange) => {
4166
setFilters((prev) => ({ ...prev, timeRange: range }));
@@ -64,27 +89,27 @@ export function AppStateProvider({ children }: { children: ReactNode }) {
6489
const [isMapSyncEnabled, setIsMapSyncEnabled] = useState(true);
6590

6691
const toggleMapSync = useCallback(() => {
67-
setIsMapSyncEnabled(prev => !prev);
92+
setIsMapSyncEnabled((prev) => !prev);
6893
}, []);
6994

7095
const selectEvent = useCallback((id: string | null) => {
7196
setSelectedEventId(id);
7297
}, []);
7398

7499
const clearFilters = useCallback(() => {
75-
setFilters(() => ({
100+
setFilters({
76101
...DEFAULT_FILTERS,
77-
// Keep search query if needed, but let's clear everything
78-
// Keep bbox if map sync is enabled? Usually reset means reset all.
79-
bbox: null
80-
}));
102+
timeRange: defaultRangeRef.current,
103+
bbox: null,
104+
});
81105
setSelectedEventId(null);
82106
}, []);
83107

84108
return (
85109
<AppStateContext.Provider
86110
value={{
87111
filters,
112+
filtersReady,
88113
selectedEventId,
89114
updateTimeRange,
90115
updateBBox,

0 commit comments

Comments
 (0)