Skip to content

Commit 11ec1ff

Browse files
authored
Merge pull request #20 from grassrootseconomics/williamluke4/3d-globe-geo-view
feat: add 3D globe geographic view with real map data
2 parents 2268390 + 81c98b0 commit 11ec1ff

21 files changed

Lines changed: 1307 additions & 38 deletions

components/dashboard/Dashboard.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import useSWR from "swr";
99
import { GearIcon, PauseIcon, PlayIcon } from "@components/icons";
1010
import { NetworkGraph2d } from "@components/network-graph/network-graph-2d";
1111
import { NetworkGraph3d } from "@components/network-graph/network-graph-3d";
12+
import { NetworkGlobe } from "@components/network-graph/network-globe";
1213

1314
import { FieldReportsOverlay } from "./FieldReportsOverlay";
1415
import { InfoPanel, type SelectedInfo } from "./InfoPanel";
@@ -19,7 +20,9 @@ import { TimelineBar } from "./TimelineBar";
1920
import { useFieldReports, useImagePreloader } from "@/hooks/dashboard";
2021
import type { DataResponse } from "@/pages/api/data";
2122
import type { FieldReportsResponse, Pool, PoolsResponse } from "@/types";
23+
import type { GlobeDataResponse, GlobePoint, GlobeArc } from "@/types/globe";
2224
import type { Voucher } from "@/types/voucher";
25+
import type { GraphType } from "./sections/DisplaySection";
2326

2427
// Fetcher function for SWR
2528
const fetcher = (url: string) => fetch(url).then((res) => res.json());
@@ -63,7 +66,17 @@ export function Dashboard() {
6366

6467
// Panel states
6568
const [optionsOpen, setOptionsOpen] = React.useState(false);
66-
const [graphType, setGraphType] = React.useState<"2D" | "3D">("2D");
69+
const [graphType, setGraphType] = React.useState<GraphType>("2D");
70+
71+
// Fetch globe geo data (only when Globe view is active)
72+
const { data: geoData } = useSWR<GlobeDataResponse>(
73+
graphType === "Globe" ? "/api/geo" : null,
74+
fetcher,
75+
{
76+
refreshInterval: 5 * 60 * 1000,
77+
revalidateOnFocus: false,
78+
}
79+
);
6780
const [showTimelineBar, setShowTimelineBar] = React.useState(true);
6881

6982
// Pool filtering
@@ -391,6 +404,36 @@ export function Dashboard() {
391404
});
392405
}, []);
393406

407+
const handleGlobePointClick = React.useCallback((point: GlobePoint) => {
408+
if (point.type === "account") {
409+
setSelectedInfo({
410+
type: "node",
411+
data: {
412+
id: point.id,
413+
value: point.value,
414+
usedVouchers: {},
415+
},
416+
});
417+
}
418+
}, []);
419+
420+
const handleGlobeArcClick = React.useCallback((arc: GlobeArc) => {
421+
setSelectedInfo({
422+
type: "link",
423+
data: {
424+
source: arc.sourceId,
425+
target: arc.targetId,
426+
token_name: arc.tokenName,
427+
token_symbol: arc.tokenSymbol,
428+
contract_address: arc.contractAddress,
429+
txCount: arc.txCount,
430+
value: arc.value,
431+
date: arc.date,
432+
dateFirst: arc.dateFirst,
433+
},
434+
});
435+
}, []);
436+
394437
const copyToClipboard = React.useCallback((text: string, field: string) => {
395438
navigator.clipboard.writeText(text);
396439
setCopiedField(field);
@@ -522,8 +565,25 @@ export function Dashboard() {
522565
/>
523566
)}
524567

525-
{/* Graph */}
526-
{graphData && graphType === "2D" ? (
568+
{/* Graph / Globe */}
569+
{graphType === "Globe" ? (
570+
<NetworkGlobe
571+
globeData={
572+
geoData?.globeData ?? {
573+
points: [],
574+
arcs: [],
575+
unmappedAccountCount: 0,
576+
totalAccountCount: 0,
577+
}
578+
}
579+
animate={animate}
580+
selectedVoucherAddresses={selectedVoucherAddresses}
581+
currentDate={date}
582+
showRecentOnly={showRecentOnly}
583+
onPointClick={handleGlobePointClick}
584+
onArcClick={handleGlobeArcClick}
585+
/>
586+
) : graphType === "2D" ? (
527587
<NetworkGraph2d
528588
animate={animate}
529589
graphData={graphData}

components/dashboard/SettingsPanel.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
DisplaySection,
1313
PhysicsSection,
1414
} from "./sections";
15+
import type { GraphType } from "./sections/DisplaySection";
1516
import type { TimelineBucket } from "./sections";
1617
import type { Pool } from "@/types";
1718
import type { Voucher } from "@/types/voucher";
@@ -60,8 +61,8 @@ export interface SettingsPanelProps {
6061
timelineHistogram: TimelineBucket[];
6162

6263
// Display
63-
graphType: "2D" | "3D";
64-
setGraphType: (type: "2D" | "3D") => void;
64+
graphType: GraphType;
65+
setGraphType: (type: GraphType) => void;
6566
showRecentOnly: boolean;
6667
setShowRecentOnly: (show: boolean) => void;
6768
showTimelineBar: boolean;
@@ -183,16 +184,18 @@ export function SettingsPanel({
183184
setShowReports={setShowReports}
184185
/>
185186

186-
{/* Physics Section */}
187-
<PhysicsSection
188-
expanded={expandedSections.physics}
189-
onToggle={() => toggleSection("physics")}
190-
inputs={physicsInputs}
191-
setChargeStrengthInput={setChargeStrengthInput}
192-
setLinkDistanceInput={setLinkDistanceInput}
193-
setCenterGravityInput={setCenterGravityInput}
194-
resetToDefaults={resetPhysicsToDefaults}
195-
/>
187+
{/* Physics Section (hidden for Globe view - no force simulation) */}
188+
{graphType !== "Globe" && (
189+
<PhysicsSection
190+
expanded={expandedSections.physics}
191+
onToggle={() => toggleSection("physics")}
192+
inputs={physicsInputs}
193+
setChargeStrengthInput={setChargeStrengthInput}
194+
setLinkDistanceInput={setLinkDistanceInput}
195+
setCenterGravityInput={setCenterGravityInput}
196+
resetToDefaults={resetPhysicsToDefaults}
197+
/>
198+
)}
196199
</div>
197200

198201
{/* Footer */}

components/dashboard/sections/DisplaySection.tsx

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
import React from "react";
66
import { ChevronDownIcon } from "@components/icons";
77

8+
export type GraphType = "2D" | "3D" | "Globe";
9+
810
export interface DisplaySectionProps {
911
expanded: boolean;
1012
onToggle: () => void;
11-
graphType: "2D" | "3D";
12-
setGraphType: (type: "2D" | "3D") => void;
13+
graphType: GraphType;
14+
setGraphType: (type: GraphType) => void;
1315
showRecentOnly: boolean;
1416
setShowRecentOnly: (show: boolean) => void;
1517
showTimelineBar: boolean;
@@ -50,26 +52,19 @@ export function DisplaySection({
5052
Graph View
5153
</label>
5254
<div className="flex gap-2">
53-
<button
54-
className={`flex-1 px-4 py-2.5 sm:py-2 rounded-md font-medium transition-colors ${
55-
graphType === "2D"
56-
? "bg-emerald-500 text-white"
57-
: "bg-white/5 text-gray-400 hover:bg-white/10"
58-
}`}
59-
onClick={() => setGraphType("2D")}
60-
>
61-
2D View
62-
</button>
63-
<button
64-
className={`flex-1 px-4 py-2.5 sm:py-2 rounded-md font-medium transition-colors ${
65-
graphType === "3D"
66-
? "bg-emerald-500 text-white"
67-
: "bg-white/5 text-gray-400 hover:bg-white/10"
68-
}`}
69-
onClick={() => setGraphType("3D")}
70-
>
71-
3D View
72-
</button>
55+
{(["2D", "3D", "Globe"] as const).map((type) => (
56+
<button
57+
key={type}
58+
className={`flex-1 px-4 py-2.5 sm:py-2 rounded-md font-medium transition-colors ${
59+
graphType === type
60+
? "bg-emerald-500 text-white"
61+
: "bg-white/5 text-gray-400 hover:bg-white/10"
62+
}`}
63+
onClick={() => setGraphType(type)}
64+
>
65+
{type === "Globe" ? "Globe" : `${type} View`}
66+
</button>
67+
))}
7368
</div>
7469
</div>
7570

components/dashboard/sections/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export { AnimationSection } from "./AnimationSection";
1212
export type { AnimationSectionProps, TimelineBucket } from "./AnimationSection";
1313

1414
export { DisplaySection } from "./DisplaySection";
15-
export type { DisplaySectionProps } from "./DisplaySection";
15+
export type { DisplaySectionProps, GraphType } from "./DisplaySection";
1616

1717
export { PhysicsSection } from "./PhysicsSection";
1818
export type { PhysicsSectionProps } from "./PhysicsSection";

0 commit comments

Comments
 (0)