Afantauzzi commited on
Commit
1ca74c0
Β·
verified Β·
1 Parent(s): ddd51b3

<!DOCTYPE html><html lang="en">

Browse files

<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Trip+Activity Visualizer β€” Uber Γ— Search Γ— Gmail</title>
<!-- Tailwind (CDN) for quick, clean UI -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Leaflet (map) -->
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
></script>
<!-- PapaParse for CSV -->
<script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
<!-- Day.js for dates -->
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.11/dayjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.11/plugin/utc.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.11/plugin/timezone.js"></script>
<script>
dayjs.extend(dayjs_plugin_utc);
dayjs.extend(dayjs_plugin_timezone);
</script>
<style>
/* Leaflet container sizing */
#map { height: 520px; }
.leaflet-div-icon { border: none; background: transparent; }
</style>
</head>
<body class="min-h-screen bg-gray-50 text-gray-900">
<header class="sticky top-0 z-10 bg-white/80 backdrop-blur border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 py-3 flex items-center gap-3">
<div class="w-9 h-9 rounded-2xl bg-gray-900 text-white grid place-items-center font-bold">TA</div>
<h1 class="text-lg md:text-xl font-semibold">Trip+Activity Visualizer β€” <span class="text-gray-500">Uber Γ— Search Γ— Gmail</span></h1>
</div>
</header> <main class="max-w-7xl mx-auto px-4 py-6 grid md:grid-cols-5 gap-6">
<!-- Controls -->
<section class="md:col-span-2 space-y-6">
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4">
<h2 class="font-semibold mb-3">1) Upload your data</h2>
<div class="space-y-3 text-sm">
<div>
<label class="block font-medium">Uber Trips (CSV)</label>
<input id="uberCsv" type="file" accept=".csv" class="mt-1 block w-full" />
<p class="text-gray-500 mt-1">Required headers (case-insensitive): <code>timestamp,pickup_lat,pickup_lng,dropoff_lat,dropoff_lng,pickup_address,dropoff_address</code></p>
</div>
<div>
<label class="block font-medium">Google Search History (JSON or CSV)</label>
<input id="searchFile" type="file" accept=".json,.csv" class="mt-1 block w-full" />
<p class="text-gray-500 mt-1">Google Takeout JSON supported (keys like <code>time,title,url</code>). CSV accepted with <code>date,query,url,source</code>.</p>
</div>
<div>
<label class="block font-medium">Gmail Index (CSV) β€” optional</label>
<input id="gmailCsv" type="file" accept=".csv" class="mt-1 block w-full" />
<p class="text-gray-500 mt-1">Headers: <code>date,from,subject,labels</code>. (Export from your mailbox tool.)</p>
</div>
</div>
<button id="loadBtn" class="mt-4 inline-flex items-center gap-2 px-3 py-2 rounded-xl bg-gray-900 text-white hover:bg-black disabled:opacity-40">
Load & Visualize
</button>
</div><div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4">
<h2 class="font-semibold mb-3">2) Filter by day & time</h2>
<div class="grid grid-cols-2 gap-3 text-sm">
<div>
<label class="block font-medium">Date</label>
<input id="dateFilter" type="date" class="mt-1 w-full border rounded-lg px-2 py-1" />
</div>
<div>
<label class="block font-medium">Hour (0–23)</label>
<input id="hourFilter" type="number" min="0" max="23" step="1" class="mt-1 w-full border rounded-lg px-2 py-1" placeholder="All" />
</div>
</div>
<div class="mt-3 flex gap-2">
<button id="applyFilters" class="px-3 py-2 rounded-xl bg-gray-900 text-white hover:bg-black">Apply</button>
<button id="clearFilters" class="px-3 py-2 rounded-xl bg-gray-200 hover:bg-gray-300">Clear</button>
</div>
</div>

<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4 text-sm">
<h2 class="font-semibold mb-3">Legend</h2>
<ul class="space-y-1">
<li class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-green-600 inline-block"></span> Pickup</li>
<li class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-red-600 inline-block"></span> Dropoff</li>
<li class="flex items-center gap-2"><span class="w-4 h-0.5 bg-blue-600 inline-block"></span> Route</li>
<li class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-indigo-600 inline-block"></span> Search query</li>
<li class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-amber-600 inline-block"></span> Gmail event</li>
</ul>
</div>

<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4 text-sm">
<h2 class="font-semibold mb-3">Export</h2>
<button id="exportMd" class="px-3 py-2 rounded-xl bg-white border hover:bg-gray-50">Download Markdown Snapshot</button>
</div>
</section>

<!-- Map & Activity -->
<section class="md:col-span-3 space-y-6">
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div id="map"></div>
</div>

<div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4">
<h2 class="font-semibold mb-3">Activity (Selected Day/Hour)</h2>
<div id="activity" class="grid md:grid-cols-2 gap-4 text-sm">
<div>
<h3 class="font-medium mb-2">Search History</h3>
<ul id="searchList" class="space-y-1 text-gray-700"></ul>
</div>
<div>
<h3 class="font-medium mb-2">Gmail</h3>
<ul id="gmailList" class="space-y-1 text-gray-700"></ul>
</div>
</div>
</div>
</section>

</main> <footer class="max-w-7xl mx-auto px-4 pb-8 text-xs text-gray-500">
<p>All data is processed locally in your browser. No uploads leave your device.</p>
</footer> <script>
// ------------------------ Utilities ------------------------
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/New_York';

function parseCsv(file) {
return new Promise((resolve, reject) => {
Papa.parse(file, {
header: true,
skipEmptyLines: true,
complete: (res) => resolve(res.data),
error: reject,
});
});
}

function parseJson(file) {
return file.text().then(txt => {
try { return JSON.parse(txt); } catch (e) { return []; }
});
}

function normalizeDate(d) {
// Accepts many formats; returns dayjs object in local tz
if (!d) return null;
const guess = dayjs(d).isValid() ? dayjs(d) : (dayjs.utc(Number(d)).isValid() ? dayjs.utc(Number(d)) : null);
return guess ? guess.tz(tz) : null;
}

function toNumber(x) { const n = Number(x); return Number.isFinite(n) ? n : null; }

function formatTime(dj) { return dj ? dj.format('YYYY-MM-DD HH:mm') : ''; }

function download(name, content) {
const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = name;
a.click();
URL.revokeObjectURL(a.href);
}

// ------------------------ State ------------------------
let uber = []; // {ts, pickup:[lat,lng], dropoff:[lat,lng], paddr, daddr}
let searches = []; // {ts, query, url, source}
let mails = []; // {ts, from, subject, labels}

// ------------------------ Map Setup ------------------------
const map = L.map('map');
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 20,
attribution: '© OpenStreetMap'
}).addTo(map);

const groupRoutes = L.layerGroup().addTo(map);
const groupPickups = L.layerGroup().addTo(map);
const groupDropoffs = L.layerGroup().addTo(map);
const groupSearches = L.layerGroup().addTo(map);
const groupMails = L.layerGroup().addTo(map);

function fitAll() {
const group = L.featureGroup([
groupRoutes, groupPickups, groupDropoffs, groupSearches, groupMails
]);
try { map.fitBounds(group.getBounds(), { padding: [20,20] }); }
catch { map.setView([40.7128, -74.006], 11); }
}

function marker(colorClass, lat, lng, title) {
const el = document.createElement('div');
el.className = `w-3 h-3 rounded-full ${colorClass} ring-2 ring-white shadow`;
return L.marker([lat, lng], { icon: L.divIcon({ html: el, iconSize: [12,12] })}).bindTooltip(title);
}

// ------------------------ Normalizers ------------------------
function ingestUber(csvRows) {
const out = [];
csvRows.forEach(r => {
const ts = normalizeDate(r.timestamp || r.date || r.datetime || r.time);
const plat = toNumber(r.pickup_lat || r.start_lat || r.origin_latitude);
const plng = toNumber(r.pickup_lng || r.start_lng || r.origin_longitude);
const dlat = toNumber(r.dropoff_lat || r.end_lat || r.destination_latitude);
const dlng = toNumber(r.dropoff_lng || r.end_lng || r.destination_longitude);
if (!ts || plat==null || plng==null || dlat==null || dlng==null) return;
out.push({
ts,
pickup:[plat, plng],
dropoff:[dlat, dlng],
paddr: r.pickup_address || r.start_address || '',
daddr: r.dropoff_address || r.end_address || '',
});
});
return out.sort((a,b)=>a.ts.valueOf()-b.ts.valueOf());
}

function ingestSearch(any) {
const out = [];
if (Array.isArray(any)) {
// CSV parsed or JSON array
any.forEach(item => {
const ts = normalizeDate(item.time || item.date || item.timestamp);
const title = item.query || item.title || '';
const url = item.url

Files changed (5) hide show
  1. README.md +8 -5
  2. components/data-upload.js +61 -0
  3. index.html +275 -19
  4. script.js +78 -0
  5. style.css +18 -28
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Triptrace Visualizer
3
- emoji: πŸ’»
4
- colorFrom: blue
5
- colorTo: red
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: TripTrace Visualizer πŸš•
3
+ colorFrom: red
4
+ colorTo: purple
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
components/data-upload.js ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class DataUpload extends HTMLElement {
2
+ connectedCallback() {
3
+ this.attachShadow({ mode: 'open' });
4
+ this.shadowRoot.innerHTML = `
5
+ <style>
6
+ :host {
7
+ display: block;
8
+ margin-bottom: 1.5rem;
9
+ }
10
+ .upload-card {
11
+ background: white;
12
+ border-radius: 1rem;
13
+ border: 1px solid #e5e7eb;
14
+ box-shadow: 0 1px 2px 0 rgba(0,0,0,0.05);
15
+ padding: 1.5rem;
16
+ }
17
+ .upload-title {
18
+ font-weight: 600;
19
+ margin-bottom: 0.75rem;
20
+ font-size: 1.125rem;
21
+ }
22
+ .upload-description {
23
+ color: #6b7280;
24
+ font-size: 0.875rem;
25
+ margin-top: 0.25rem;
26
+ }
27
+ .file-input {
28
+ margin-top: 0.5rem;
29
+ width: 100%;
30
+ border: 1px solid #e5e7eb;
31
+ border-radius: 0.5rem;
32
+ padding: 0.5rem 0.75rem;
33
+ }
34
+ .upload-button {
35
+ margin-top: 1rem;
36
+ background: #6366f1;
37
+ color: white;
38
+ border: none;
39
+ border-radius: 0.75rem;
40
+ padding: 0.5rem 1rem;
41
+ cursor: pointer;
42
+ transition: background 0.2s;
43
+ }
44
+ .upload-button:hover {
45
+ background: #4f46e5;
46
+ }
47
+ </style>
48
+ <div class="upload-card">
49
+ <h2 class="upload-title">Upload Your Data</h2>
50
+ <div>
51
+ <label>Uber Trips (CSV)</label>
52
+ <input type="file" accept=".csv" class="file-input">
53
+ <p class="upload-description">Trip history with coordinates and timestamps</p>
54
+ </div>
55
+ <button class="upload-button">Process Data</button>
56
+ </div>
57
+ `;
58
+ }
59
+ }
60
+
61
+ customElements.define('data-upload', DataUpload);
index.html CHANGED
@@ -1,19 +1,275 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>TripTrace Visualizer β€” Uber Γ— Search Γ— Gmail</title>
7
+ <!-- Tailwind CSS -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ primary: '#6366F1', // indigo-500
15
+ secondary: '#F59E0B' // amber-500
16
+ }
17
+ }
18
+ }
19
+ }
20
+ </script>
21
+ <!-- Leaflet (map) -->
22
+ <link
23
+ rel="stylesheet"
24
+ href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
25
+ integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
26
+ crossorigin=""
27
+ />
28
+ <script
29
+ src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
30
+ integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
31
+ crossorigin=""
32
+ ></script>
33
+ <!-- PapaParse for CSV -->
34
+ <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
35
+ <!-- Day.js for dates -->
36
+ <script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.11/dayjs.min.js"></script>
37
+ <script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.11/plugin/utc.js"></script>
38
+ <script src="https://cdn.jsdelivr.net/npm/dayjs@1.11.11/plugin/timezone.js"></script>
39
+ <style>
40
+ /* Leaflet container sizing */
41
+ #map { height: 520px; }
42
+ .leaflet-div-icon { border: none; background: transparent; }
43
+ </style>
44
+ </head>
45
+ <body class="min-h-screen bg-gray-50 text-gray-900">
46
+ <header class="sticky top-0 z-10 bg-white/80 backdrop-blur border-b border-gray-200">
47
+ <div class="max-w-7xl mx-auto px-4 py-3 flex items-center gap-3">
48
+ <div class="w-9 h-9 rounded-2xl bg-primary text-white grid place-items-center font-bold">TT</div>
49
+ <h1 class="text-lg md:text-xl font-semibold">TripTrace Visualizer β€” <span class="text-gray-500">Uber Γ— Search Γ— Gmail</span></h1>
50
+ </div>
51
+ </header>
52
+
53
+ <main class="max-w-7xl mx-auto px-4 py-6 grid md:grid-cols-5 gap-6">
54
+ <!-- Controls -->
55
+ <section class="md:col-span-2 space-y-6">
56
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4">
57
+ <h2 class="font-semibold mb-3">1) Upload your data</h2>
58
+ <div class="space-y-3 text-sm">
59
+ <div>
60
+ <label class="block font-medium">Uber Trips (CSV)</label>
61
+ <input id="uberCsv" type="file" accept=".csv" class="mt-1 block w-full" />
62
+ <p class="text-gray-500 mt-1">Required headers (case-insensitive): <code>timestamp,pickup_lat,pickup_lng,dropoff_lat,dropoff_lng,pickup_address,dropoff_address</code></p>
63
+ </div>
64
+ <div>
65
+ <label class="block font-medium">Google Search History (JSON or CSV)</label>
66
+ <input id="searchFile" type="file" accept=".json,.csv" class="mt-1 block w-full" />
67
+ <p class="text-gray-500 mt-1">Google Takeout JSON supported (keys like <code>time,title,url</code>). CSV accepted with <code>date,query,url,source</code>.</p>
68
+ </div>
69
+ <div>
70
+ <label class="block font-medium">Gmail Index (CSV) β€” optional</label>
71
+ <input id="gmailCsv" type="file" accept=".csv" class="mt-1 block w-full" />
72
+ <p class="text-gray-500 mt-1">Headers: <code>date,from,subject,labels</code>. (Export from your mailbox tool.)</p>
73
+ </div>
74
+ </div>
75
+ <button id="loadBtn" class="mt-4 inline-flex items-center gap-2 px-3 py-2 rounded-xl bg-primary text-white hover:bg-primary-600 disabled:opacity-40">
76
+ Load & Visualize
77
+ </button>
78
+ </div>
79
+
80
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4">
81
+ <h2 class="font-semibold mb-3">2) Filter by day & time</h2>
82
+ <div class="grid grid-cols-2 gap-3 text-sm">
83
+ <div>
84
+ <label class="block font-medium">Date</label>
85
+ <input id="dateFilter" type="date" class="mt-1 w-full border rounded-lg px-2 py-1" />
86
+ </div>
87
+ <div>
88
+ <label class="block font-medium">Hour (0–23)</label>
89
+ <input id="hourFilter" type="number" min="0" max="23" step="1" class="mt-1 w-full border rounded-lg px-2 py-1" placeholder="All" />
90
+ </div>
91
+ </div>
92
+ <div class="mt-3 flex gap-2">
93
+ <button id="applyFilters" class="px-3 py-2 rounded-xl bg-primary text-white hover:bg-primary-600">Apply</button>
94
+ <button id="clearFilters" class="px-3 py-2 rounded-xl bg-gray-200 hover:bg-gray-300">Clear</button>
95
+ </div>
96
+ </div>
97
+
98
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4 text-sm">
99
+ <h2 class="font-semibold mb-3">Legend</h2>
100
+ <ul class="space-y-1">
101
+ <li class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-green-600 inline-block"></span> Pickup</li>
102
+ <li class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-red-600 inline-block"></span> Dropoff</li>
103
+ <li class="flex items-center gap-2"><span class="w-4 h-0.5 bg-blue-600 inline-block"></span> Route</li>
104
+ <li class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-primary inline-block"></span> Search query</li>
105
+ <li class="flex items-center gap-2"><span class="w-3 h-3 rounded-full bg-secondary inline-block"></span> Gmail event</li>
106
+ </ul>
107
+ </div>
108
+
109
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4 text-sm">
110
+ <h2 class="font-semibold mb-3">Export</h2>
111
+ <button id="exportMd" class="px-3 py-2 rounded-xl bg-white border hover:bg-gray-50">Download Markdown Snapshot</button>
112
+ </div>
113
+ </section>
114
+
115
+ <!-- Map & Activity -->
116
+ <section class="md:col-span-3 space-y-6">
117
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
118
+ <div id="map"></div>
119
+ </div>
120
+
121
+ <div class="bg-white rounded-2xl shadow-sm border border-gray-200 p-4">
122
+ <h2 class="font-semibold mb-3">Activity (Selected Day/Hour)</h2>
123
+ <div id="activity" class="grid md:grid-cols-2 gap-4 text-sm">
124
+ <div>
125
+ <h3 class="font-medium mb-2">Search History</h3>
126
+ <ul id="searchList" class="space-y-1 text-gray-700"></ul>
127
+ </div>
128
+ <div>
129
+ <h3 class="font-medium mb-2">Gmail</h3>
130
+ <ul id="gmailList" class="space-y-1 text-gray-700"></ul>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </section>
135
+ </main>
136
+
137
+ <footer class="max-w-7xl mx-auto px-4 pb-8 text-xs text-gray-500">
138
+ <p>All data is processed locally in your browser. No uploads leave your device.</p>
139
+ </footer>
140
+
141
+ <script>
142
+ // ------------------------ Utilities ------------------------
143
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'America/New_York';
144
+
145
+ function parseCsv(file) {
146
+ return new Promise((resolve, reject) => {
147
+ Papa.parse(file, {
148
+ header: true,
149
+ skipEmptyLines: true,
150
+ complete: (res) => resolve(res.data),
151
+ error: reject,
152
+ });
153
+ });
154
+ }
155
+
156
+ function parseJson(file) {
157
+ return file.text().then(txt => {
158
+ try { return JSON.parse(txt); } catch (e) { return []; }
159
+ });
160
+ }
161
+
162
+ function normalizeDate(d) {
163
+ if (!d) return null;
164
+ const guess = dayjs(d).isValid() ? dayjs(d) : (dayjs.utc(Number(d)).isValid() ? dayjs.utc(Number(d)) : null);
165
+ return guess ? guess.tz(tz) : null;
166
+ }
167
+
168
+ function toNumber(x) { const n = Number(x); return Number.isFinite(n) ? n : null; }
169
+
170
+ function formatTime(dj) { return dj ? dj.format('YYYY-MM-DD HH:mm') : ''; }
171
+
172
+ function download(name, content) {
173
+ const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
174
+ const a = document.createElement('a');
175
+ a.href = URL.createObjectURL(blob);
176
+ a.download = name;
177
+ a.click();
178
+ URL.revokeObjectURL(a.href);
179
+ }
180
+
181
+ // ------------------------ State ------------------------
182
+ let uber = []; // {ts, pickup:[lat,lng], dropoff:[lat,lng], paddr, daddr}
183
+ let searches = []; // {ts, query, url, source}
184
+ let mails = []; // {ts, from, subject, labels}
185
+
186
+ // ------------------------ Map Setup ------------------------
187
+ const map = L.map('map');
188
+ const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
189
+ maxZoom: 20,
190
+ attribution: '&copy; OpenStreetMap'
191
+ }).addTo(map);
192
+
193
+ const groupRoutes = L.layerGroup().addTo(map);
194
+ const groupPickups = L.layerGroup().addTo(map);
195
+ const groupDropoffs = L.layerGroup().addTo(map);
196
+ const groupSearches = L.layerGroup().addTo(map);
197
+ const groupMails = L.layerGroup().addTo(map);
198
+
199
+ function fitAll() {
200
+ const group = L.featureGroup([
201
+ groupRoutes, groupPickups, groupDropoffs, groupSearches, groupMails
202
+ ]);
203
+ try { map.fitBounds(group.getBounds(), { padding: [20,20] }); }
204
+ catch { map.setView([40.7128, -74.006], 11); }
205
+ }
206
+
207
+ function marker(colorClass, lat, lng, title) {
208
+ const el = document.createElement('div');
209
+ el.className = `w-3 h-3 rounded-full ${colorClass} ring-2 ring-white shadow`;
210
+ return L.marker([lat, lng], { icon: L.divIcon({ html: el, iconSize: [12,12] })}).bindTooltip(title);
211
+ }
212
+
213
+ function render(dayStr, hour) {
214
+ [groupRoutes, groupPickups, groupDropoffs, groupSearches, groupMails].forEach(g=>g.clearLayers());
215
+ document.getElementById('searchList').innerHTML = '';
216
+ document.getElementById('gmailList').innerHTML = '';
217
+
218
+ const day = dayStr ? dayjs(dayStr).startOf('day') : null;
219
+
220
+ const inWindow = (ts) => {
221
+ if (!day) return true;
222
+ if (hour==='' || hour===null || Number.isNaN(Number(hour))) {
223
+ return ts.isSame(day, 'day');
224
+ } else {
225
+ const start = day.add(hour, 'hour');
226
+ const end = start.add(1, 'hour');
227
+ return ts.isAfter(start) && ts.isBefore(end);
228
+ }
229
+ };
230
+
231
+ // Uber trips
232
+ const tripBounds = [];
233
+ uber.filter(u => inWindow(u.ts)).forEach(u => {
234
+ const line = L.polyline([u.pickup, u.dropoff], { color: '#2563eb', weight: 3, opacity: 0.8 });
235
+ line.addTo(groupRoutes);
236
+ const m1 = marker('bg-green-600', u.pickup[0], u.pickup[1], `${formatTime(u.ts)}\nPickup\n${u.paddr || ''}`).addTo(groupPickups);
237
+ const m2 = marker('bg-red-600', u.dropoff[0], u.dropoff[1], `${formatTime(u.ts)}\nDropoff\n${u.daddr || ''}`).addTo(groupDropoffs);
238
+ tripBounds.push(u.pickup, u.dropoff);
239
+ });
240
+
241
+ // Searches
242
+ searches.filter(s => inWindow(s.ts)).forEach(s => {
243
+ const li = document.createElement('li');
244
+ li.innerHTML = `<span class="inline-flex w-2 h-2 rounded-full bg-primary mr-2 align-middle"></span>${formatTime(s.ts)} β€” <span class="font-medium">${escapeHtml(s.query)}</span> ${s.url ? `Β· <a class='text-primary-700 underline' href='${s.url}' target='_blank' rel='noreferrer'>link</a>`:''}`;
245
+ document.getElementById('searchList').appendChild(li);
246
+ });
247
+
248
+ // Gmail
249
+ mails.filter(m => inWindow(m.ts)).forEach(m => {
250
+ const li = document.createElement('li');
251
+ li.innerHTML = `<span class="inline-flex w-2 h-2 rounded-full bg-secondary mr-2 align-middle"></span>${formatTime(m.ts)} β€” <span class="font-medium">${escapeHtml(m.from)}</span>: ${escapeHtml(m.subject)} ${m.labels?`<span class='text-gray-500'>(${escapeHtml(m.labels)})</span>`:''}`;
252
+ document.getElementById('gmailList').appendChild(li);
253
+ });
254
+
255
+ // Fit map
256
+ if (tripBounds.length) {
257
+ const bounds = L.latLngBounds(tripBounds);
258
+ map.fitBounds(bounds.pad(0.25));
259
+ } else {
260
+ fitAll();
261
+ }
262
+ }
263
+
264
+ function escapeHtml(str) {
265
+ return (str||'').replace(/[&<>"']/g, (c)=>({
266
+ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&#39;'
267
+ })[c]);
268
+ }
269
+
270
+ // Initialize map center
271
+ map.setView([40.7128, -74.006], 11);
272
+ </script>
273
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
274
+ </body>
275
+ </html>
script.js ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Main application functionality for TripTrace Visualizer
2
+ document.addEventListener('DOMContentLoaded', () => {
3
+ // Initialize day.js plugins
4
+ dayjs.extend(dayjs_plugin_utc);
5
+ dayjs.extend(dayjs_plugin_timezone);
6
+
7
+ // Event listeners
8
+ document.getElementById('loadBtn').addEventListener('click', handleFileUpload);
9
+ document.getElementById('applyFilters').addEventListener('click', applyFilters);
10
+ document.getElementById('clearFilters').addEventListener('click', clearFilters);
11
+ document.getElementById('exportMd').addEventListener('click', exportMarkdown);
12
+
13
+ // Set default date to today
14
+ document.getElementById('dateFilter').value = dayjs().format('YYYY-MM-DD');
15
+ });
16
+
17
+ async function handleFileUpload() {
18
+ const uberFile = document.getElementById('uberCsv').files[0];
19
+ if (!uberFile) {
20
+ alert('Please select the Uber CSV first.');
21
+ return;
22
+ }
23
+
24
+ const searchFile = document.getElementById('searchFile').files[0];
25
+ const gmailFile = document.getElementById('gmailCsv').files[0];
26
+
27
+ try {
28
+ // Process Uber data
29
+ const uberRows = await parseCsv(uberFile);
30
+ uber = ingestUber(uberRows);
31
+
32
+ // Process Search data
33
+ searches = [];
34
+ if (searchFile) {
35
+ if (searchFile.name.toLowerCase().endsWith('.json')) {
36
+ const j = await parseJson(searchFile);
37
+ searches = ingestSearch(j);
38
+ } else {
39
+ const srows = await parseCsv(searchFile);
40
+ searches = ingestSearch(srows);
41
+ }
42
+ }
43
+
44
+ // Process Gmail data
45
+ mails = [];
46
+ if (gmailFile) {
47
+ const grows = await parseCsv(gmailFile);
48
+ mails = ingestGmail(grows);
49
+ }
50
+
51
+ // Set default date to first trip day if available
52
+ if (uber.length) {
53
+ document.getElementById('dateFilter').value = uber[0].ts.format('YYYY-MM-DD');
54
+ }
55
+
56
+ // Initial render
57
+ render(document.getElementById('dateFilter').value, document.getElementById('hourFilter').value);
58
+
59
+ } catch (error) {
60
+ console.error('Error processing files:', error);
61
+ alert('Error processing files. Please check the console for details.');
62
+ }
63
+ }
64
+
65
+ function applyFilters() {
66
+ render(
67
+ document.getElementById('dateFilter').value,
68
+ document.getElementById('hourFilter').value
69
+ );
70
+ }
71
+
72
+ function clearFilters() {
73
+ document.getElementById('dateFilter').value = '';
74
+ document.getElementById('hourFilter').value = '';
75
+ render('', '');
76
+ }
77
+
78
+ // ... (rest of the utility functions from original script)
style.css CHANGED
@@ -1,28 +1,18 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }
 
1
+ /* Custom styles for TripTrace Visualizer */
2
+ @layer components {
3
+ .btn-primary {
4
+ @apply bg-primary hover:bg-primary-600 text-white px-4 py-2 rounded-xl transition-colors;
5
+ }
6
+
7
+ .btn-secondary {
8
+ @apply bg-secondary hover:bg-secondary-600 text-white px-4 py-2 rounded-xl transition-colors;
9
+ }
10
+
11
+ .card {
12
+ @apply bg-white rounded-2xl shadow-sm border border-gray-200 p-4;
13
+ }
14
+
15
+ .map-container {
16
+ @apply h-[520px] w-full rounded-2xl overflow-hidden;
17
+ }
18
+ }