donnyb commited on
Commit
6b22e52
·
1 Parent(s): 6ca2e4d

app files for falconvis

Browse files
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
.vscode/extensions.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "recommendations": ["svelte.svelte-vscode"]
3
+ }
DockerFile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ WORKDIR /code
4
+
5
+ COPY ./requirements.txt /code/requirements.txt
6
+
7
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
8
+
9
+ COPY . .
10
+
11
+ CMD ["python3", "server.py", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,6 @@
1
- ---
2
- title: Flights Duckdb
3
- emoji: 📚
4
- colorFrom: green
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
1
+ # 1m flights from [todo](find link)
 
 
 
 
 
 
 
2
 
3
+ ```bash
4
+ yarn
5
+ yarn dev --open
6
+ ```
index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
package.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "flights-30m",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "check": "svelte-check --tsconfig ./tsconfig.json"
11
+ },
12
+ "dependencies": {
13
+ "d3": "^7.8.4",
14
+ "falcon-vis": "0.17.1",
15
+ "svelte-vega": "^1.2.0",
16
+ "vega": "^5.24.0",
17
+ "vega-lite": "^5.6.1"
18
+ },
19
+ "devDependencies": {
20
+ "@sveltejs/vite-plugin-svelte": "^2.0.2",
21
+ "@tsconfig/svelte": "^3.0.0",
22
+ "svelte": "^3.55.1",
23
+ "svelte-check": "^2.10.3",
24
+ "tslib": "^2.5.0",
25
+ "typescript": "^4.9.3",
26
+ "vite": "^4.1.0"
27
+ }
28
+ }
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ duckdb
4
+ fire
server.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ Backend server for the frontend app
2
+ This file contains the endpoints that can be called via HTTP
3
+ """
4
+
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from fastapi.responses import Response
8
+ import duckdb
9
+ import pyarrow as pa
10
+ from fire import Fire
11
+ from uvicorn import run
12
+
13
+ app = FastAPI()
14
+
15
+ origins = ["*"]
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=origins,
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ # setup global connection to the database with a table
25
+ con = duckdb.connect()
26
+ con.query("""CREATE TABLE flights AS FROM 'flights-30m.parquet'""")
27
+
28
+
29
+ @app.get("/query/{sql_query:path}")
30
+ async def query(sql_query: str):
31
+ global con
32
+ sql_query = sql_query.replace("count(*)", "count(*)::INT")
33
+ result = con.query(sql_query).arrow()
34
+ return Response(arrow_to_bytes(result), media_type="application/octet-stream")
35
+
36
+
37
+ def arrow_to_bytes(table: pa.Table):
38
+ sink = pa.BufferOutputStream()
39
+ with pa.RecordBatchStreamWriter(sink, table.schema) as writer:
40
+ writer.write_table(table)
41
+ bytes = sink.getvalue().to_pybytes()
42
+ return bytes
43
+
44
+
45
+ def serve(port=8000, host="localhost"):
46
+ run(app, port=port, host=host)
47
+
48
+
49
+ if __name__ == "__main__":
50
+ Fire(serve) # so I can run cli args with it
src/App.svelte ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { FalconVis, HttpDB } from "falcon-vis";
3
+ import type { View0D, View1D, View0DState, View1DState } from "falcon-vis";
4
+ import GithubButton from "./components/GithubButton.svelte";
5
+ import { tableFromIPC } from "apache-arrow";
6
+
7
+ import { onMount } from "svelte";
8
+ import Histogram from "./components/Histogram.svelte";
9
+ import TotalCount from "./components/TotalCount.svelte";
10
+ import UsMapVis from "./components/USMapVis.svelte";
11
+
12
+ let falcon: FalconVis;
13
+ let countView: View0D;
14
+ let distanceView: View1D;
15
+ let arrDelayView: View1D;
16
+ let depDelayView: View1D;
17
+ let flightDateView: View1D;
18
+ let originView: View1D;
19
+ let countState: View0DState;
20
+ let distanceState: View1DState;
21
+ let arrDelayState: View1DState;
22
+ let depDelayState: View1DState;
23
+ let flightDateState: View1DState;
24
+ let originState: View1DState;
25
+
26
+ onMount(async () => {
27
+ const dontEncodeURL = (q) => q;
28
+ const db = new HttpDB(
29
+ `http://localhost:8000/query/`,
30
+ "flights",
31
+ new Map([["FlightDate", "epoch(FlightDate)*1000"]]),
32
+ dontEncodeURL
33
+ );
34
+ falcon = new FalconVis(db);
35
+
36
+ countView = await falcon.view0D((updated) => {
37
+ countState = updated;
38
+ });
39
+
40
+ distanceView = await falcon.view1D({
41
+ type: "continuous",
42
+ name: "Distance",
43
+ resolution: 400,
44
+ bins: 5,
45
+ });
46
+ distanceView.onChange((updated) => {
47
+ distanceState = updated;
48
+ });
49
+
50
+ arrDelayView = await falcon.view1D({
51
+ type: "continuous",
52
+ name: "ArrDelay",
53
+ resolution: 400,
54
+ range: [-20, 60],
55
+ bins: 5,
56
+ });
57
+ arrDelayView.onChange((updated) => {
58
+ arrDelayState = updated;
59
+ });
60
+
61
+ depDelayView = await falcon.view1D({
62
+ type: "continuous",
63
+ name: "DepDelay",
64
+ resolution: 400,
65
+ range: [-20, 60],
66
+ bins: 5,
67
+ });
68
+ depDelayView.onChange((updated) => {
69
+ depDelayState = updated;
70
+ });
71
+
72
+ flightDateView = await falcon.view1D({
73
+ type: "continuous",
74
+ name: "FlightDate",
75
+ resolution: 400,
76
+ bins: 25,
77
+ time: true,
78
+ });
79
+ flightDateView.onChange((updated) => {
80
+ flightDateState = updated;
81
+ });
82
+
83
+ originView = await falcon.view1D({
84
+ type: "categorical",
85
+ name: "OriginState",
86
+ });
87
+ originView.onChange((updated) => {
88
+ originState = updated;
89
+ });
90
+
91
+ await falcon.link();
92
+
93
+ entries = await falcon.entries({
94
+ length: numEntries,
95
+ offset: page,
96
+ });
97
+ });
98
+
99
+ let page = 0;
100
+ let numEntries = 25;
101
+ let entries: Iterable<Record<string, any>>;
102
+ let resolved = true;
103
+ async function updateEntriesWhenStateChanges(
104
+ viewStates: View1DState[],
105
+ delay = 0
106
+ ) {
107
+ // make a request for entries
108
+ if (falcon && resolved) {
109
+ resolved = false;
110
+ entries = await falcon.entries({
111
+ length: numEntries,
112
+ offset: page,
113
+ });
114
+ await new Promise((resolve) => setTimeout(resolve, delay));
115
+ resolved = true;
116
+ }
117
+ }
118
+ $: updateEntriesWhenStateChanges([
119
+ distanceState,
120
+ originState,
121
+ arrDelayState,
122
+ depDelayState,
123
+ flightDateState,
124
+ ]);
125
+ let tableKeys = [
126
+ "FlightDate",
127
+ "OriginState",
128
+ "DestState",
129
+ "DepDelay",
130
+ "ArrDelay",
131
+ "Distance",
132
+ ];
133
+ </script>
134
+
135
+ <svelte:head>
136
+ <title>FalconVis | 30 million</title>
137
+ </svelte:head>
138
+
139
+ <header>
140
+ <div>
141
+ <a href="https://github.com/cmudig/falcon#falconvis" target="_blank">
142
+ <img
143
+ src="https://user-images.githubusercontent.com/65095341/224896033-afc8bd8e-d0e0-4031-a7b2-3857bef51327.svg"
144
+ alt="FalconVis Logo"
145
+ height="60px"
146
+ />
147
+ </a>
148
+ </div>
149
+ <GithubButton href="https://github.com/cmudig/falcon" width={40} />
150
+ </header>
151
+
152
+ <main>
153
+ <!-- section for all the visualizations -->
154
+ <div id="vis">
155
+ <div id="charts">
156
+ <div id="hists">
157
+ {#if falcon && distanceState}
158
+ <Histogram
159
+ title="Distance Flown"
160
+ dimLabel="Distance in miles"
161
+ bins={distanceState.bin}
162
+ filteredCounts={distanceState.filter}
163
+ totalCounts={distanceState.total}
164
+ on:mouseenter={async () => {
165
+ await distanceView.activate();
166
+ }}
167
+ on:select={async (e) => {
168
+ const selection = e.detail;
169
+ if (selection !== null) {
170
+ await distanceView.select(selection);
171
+ } else {
172
+ await distanceView.select();
173
+ }
174
+ }}
175
+ />
176
+ {/if}
177
+
178
+ {#if falcon && arrDelayState}
179
+ <Histogram
180
+ title="Arrival Flight Delay"
181
+ dimLabel="Delay in + minutes"
182
+ bins={arrDelayState.bin}
183
+ filteredCounts={arrDelayState.filter}
184
+ totalCounts={arrDelayState.total}
185
+ on:mouseenter={async () => {
186
+ await arrDelayView.activate();
187
+ }}
188
+ on:select={async (e) => {
189
+ const selection = e.detail;
190
+ if (selection !== null) {
191
+ await arrDelayView.select(selection);
192
+ } else {
193
+ await arrDelayView.select();
194
+ }
195
+ }}
196
+ />
197
+ {/if}
198
+
199
+ {#if falcon && depDelayState}
200
+ <Histogram
201
+ title="Departure Flight Delay"
202
+ dimLabel="Delay in + minutes"
203
+ bins={depDelayState.bin}
204
+ filteredCounts={depDelayState.filter}
205
+ totalCounts={depDelayState.total}
206
+ on:mouseenter={async () => {
207
+ await depDelayView.activate();
208
+ }}
209
+ on:select={async (e) => {
210
+ const selection = e.detail;
211
+ if (selection !== null) {
212
+ await depDelayView.select(selection);
213
+ } else {
214
+ await depDelayView.select();
215
+ }
216
+ }}
217
+ />
218
+ {/if}
219
+
220
+ {#if falcon && flightDateState}
221
+ <Histogram
222
+ timeUnit=""
223
+ type="temporal"
224
+ title="Flight Date"
225
+ dimLabel="Time of flight"
226
+ bins={flightDateState.bin}
227
+ filteredCounts={flightDateState.filter}
228
+ totalCounts={flightDateState.total}
229
+ on:mouseenter={async () => {
230
+ await flightDateView.activate();
231
+ }}
232
+ on:select={async (e) => {
233
+ const selection = e.detail;
234
+ if (selection !== null) {
235
+ await flightDateView.select(selection);
236
+ } else {
237
+ await flightDateView.select();
238
+ }
239
+ }}
240
+ />
241
+ {/if}
242
+ </div>
243
+
244
+ <div id="maps">
245
+ {#if falcon && originState}
246
+ <UsMapVis
247
+ width={700}
248
+ title="Origin Airport Location by State"
249
+ state={originState}
250
+ on:mouseenter={async () => {
251
+ await originView.activate();
252
+ }}
253
+ on:select={async (e) => {
254
+ const selection = e.detail;
255
+ if (selection !== null) {
256
+ await originView.select(selection);
257
+ } else {
258
+ await originView.select();
259
+ }
260
+ }}
261
+ />
262
+ {/if}
263
+ </div>
264
+ </div>
265
+
266
+ <!-- section for all entries in the table -->
267
+ <div id="table">
268
+ <div>
269
+ <TotalCount
270
+ filteredCount={countState?.filter ?? 0}
271
+ totalCount={countState?.total ?? 0}
272
+ width={800}
273
+ height={20}
274
+ />
275
+ </div>
276
+ {#if entries}
277
+ <div>
278
+ <button
279
+ on:click={async () => {
280
+ page = Math.max(page - numEntries, 0);
281
+ entries = await falcon.entries({
282
+ length: numEntries,
283
+ offset: page,
284
+ });
285
+ }}>back</button
286
+ >
287
+ <button
288
+ on:click={async () => {
289
+ page += numEntries;
290
+ entries = await falcon.entries({
291
+ length: numEntries,
292
+ offset: page,
293
+ });
294
+ }}>next</button
295
+ >
296
+ </div>
297
+ <div id="images">
298
+ <table id="table">
299
+ {#if entries && countState}
300
+ <tr>
301
+ {#each tableKeys as key}
302
+ <th>{key}</th>
303
+ {/each}
304
+ </tr>
305
+ {#each Array.from(entries) as instance}
306
+ <tr>
307
+ <td
308
+ >{new Date(
309
+ instance["FlightDate"]
310
+ ).toDateString()}</td
311
+ >
312
+ {#each tableKeys.slice(1) as key}
313
+ <td>{instance[key]}</td>
314
+ {/each}
315
+ </tr>
316
+ {/each}
317
+ {/if}
318
+ </table>
319
+ </div>
320
+ {/if}
321
+ </div>
322
+ </div>
323
+ </main>
324
+
325
+ <style>
326
+ :global(:root) {
327
+ --bg-color: white;
328
+ --text-color: rgb(53, 53, 53);
329
+ --primary-color: var(--text-color);
330
+ }
331
+ :global(body, html) {
332
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
333
+ Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
334
+ margin: 0;
335
+ background-color: var(--bg-color);
336
+ color: var(--text-color);
337
+ /* padding: 5px; */
338
+ }
339
+ main {
340
+ padding: 15px;
341
+ }
342
+ header {
343
+ display: flex;
344
+ justify-content: space-between;
345
+ align-items: center;
346
+ background-color: rgb(250, 250, 250);
347
+ border-radius: 5px;
348
+ box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.115);
349
+ margin: 10px;
350
+ margin-bottom: 5px;
351
+ padding: 15px;
352
+ padding-right: 25px;
353
+ }
354
+ #table {
355
+ border: 1px solid lightgrey;
356
+ margin-top: 20px;
357
+ padding: 20px;
358
+ border-radius: 10px;
359
+ }
360
+ #vis {
361
+ width: 100%;
362
+ height: 500px;
363
+ }
364
+
365
+ #charts {
366
+ display: flex;
367
+ gap: 20px;
368
+ }
369
+ #maps {
370
+ /* padding: 20px; */
371
+ }
372
+ #hists {
373
+ display: flex;
374
+ flex-wrap: wrap;
375
+ gap: 10px;
376
+ }
377
+ table {
378
+ border-collapse: collapse;
379
+ width: 100%;
380
+ table-layout: fixed;
381
+ }
382
+
383
+ td,
384
+ th {
385
+ border: 1px solid #dddddd;
386
+ text-align: left;
387
+ padding: 8px;
388
+ }
389
+ th {
390
+ font-weight: 500;
391
+ background-color: #f9f9f9;
392
+ }
393
+ </style>
src/app.css ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url("https://fonts.googleapis.com/css2?family=Exo:ital,wght@1,600&display=swap");
2
+
3
+ :root {
4
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
5
+ line-height: 1.5;
6
+ font-weight: 400;
7
+
8
+ /* color-scheme: light dark; */
9
+ /* color: rgba(255, 255, 255, 0.87); */
10
+ color: #505050;
11
+
12
+ font-synthesis: none;
13
+ text-rendering: optimizeLegibility;
14
+ -webkit-font-smoothing: antialiased;
15
+ -moz-osx-font-smoothing: grayscale;
16
+ -webkit-text-size-adjust: 100%;
17
+ }
18
+
19
+ button {
20
+ border-radius: 8px;
21
+ border: 1px solid transparent;
22
+ padding: 0.6em 1.2em;
23
+ font-size: 1em;
24
+ font-weight: 500;
25
+ font-family: inherit;
26
+ cursor: pointer;
27
+ transition: border-color 0.25s;
28
+ }
29
+ button:hover {
30
+ border-color: var(--primary-color);
31
+ }
32
+ button:focus,
33
+ button:focus-visible {
34
+ outline: 4px auto -webkit-focus-ring-color;
35
+ }
src/assets/github-mark.svg ADDED
src/components/CategoricalHistogram.svelte ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import VLCategorical from "./VLCategorical.svelte";
3
+ import type { CategoricalView1DState } from "falcon-vis/src/core/views/view1D";
4
+
5
+ export let state: CategoricalView1DState;
6
+
7
+ export let title = "";
8
+ export let width = 400;
9
+ export let height = 125;
10
+ export let countLabel = "Count";
11
+ export let dimLabel = "";
12
+ export let labelColor = "hsla(0, 0%, 100%, 0.9)";
13
+ export let backgroundBarColor = "hsla(0, 0%, 100%, 0.5)";
14
+ export let foregroundBarColor = "hsla(172, 97%, 45%, 0.95)";
15
+ export let backgroundColor = "hsl(240,23%,9%)";
16
+ export let onlyFiltered = false;
17
+
18
+ $: bins =
19
+ state?.bin
20
+ ?.filter((b) => b !== null)
21
+ .map((b, i) => ({
22
+ bin: b,
23
+ count: state["total"][i],
24
+ filteredCount: state["filter"][i],
25
+ })) ?? [];
26
+ </script>
27
+
28
+ <VLCategorical
29
+ {bins}
30
+ {title}
31
+ {width}
32
+ {height}
33
+ {countLabel}
34
+ {dimLabel}
35
+ {labelColor}
36
+ {backgroundBarColor}
37
+ {foregroundBarColor}
38
+ {backgroundColor}
39
+ {onlyFiltered}
40
+ on:mouseenter
41
+ on:mouseleave
42
+ on:mouseup
43
+ on:mousedown
44
+ on:click
45
+ on:select
46
+ />
src/components/ContinuousHistogram.svelte ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import VLContinuous from "./VLContinuous.svelte";
3
+ import type { View1DState } from "falcon-vis";
4
+ export let state: View1DState;
5
+
6
+ export let title = "";
7
+ export let width = 400;
8
+ export let height = 125;
9
+ export let countLabel = "Count";
10
+ export let dimLabel = "";
11
+ export let onlyFiltered = false;
12
+ export let type = "quantitative";
13
+ export let timeUnit = "";
14
+
15
+ $: data = convertFormat(state);
16
+
17
+ function convertFormat(state: View1DState) {
18
+ let newBins = [];
19
+ if (state) {
20
+ for (let i = 0; i < state.bin.length; i++) {
21
+ newBins.push({
22
+ bin: [state.bin[i].binStart, state.bin[i].binEnd],
23
+ count: state.total[i],
24
+ filteredCount: state.filter[i],
25
+ });
26
+ }
27
+ }
28
+ return newBins;
29
+ }
30
+ </script>
31
+
32
+ <VLContinuous
33
+ bins={data}
34
+ on:select
35
+ on:mouseenter
36
+ on:mouseleave
37
+ on:mouseup
38
+ on:mousedown
39
+ on:click
40
+ {timeUnit}
41
+ {type}
42
+ {title}
43
+ {width}
44
+ {height}
45
+ {countLabel}
46
+ {dimLabel}
47
+ {onlyFiltered}
48
+ />
src/components/GithubButton.svelte ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import Logo from "../assets/github-mark.svg";
3
+
4
+ export let href: string = "";
5
+ export let width: number = 25;
6
+ </script>
7
+
8
+ <a {href} target="_blank">
9
+ <img src={Logo} alt="github logo" {width} />
10
+ </a>
11
+
12
+ <style>
13
+ /* put stuff here */
14
+ </style>
src/components/Histogram.svelte ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import TitleBar from "./TitleBar.svelte";
3
+ import ContinuousHistogram from "./ContinuousHistogram.svelte";
4
+
5
+ export let dimLabel = "";
6
+ export let title = "";
7
+
8
+ export let totalCounts: Uint32Array;
9
+ export let filteredCounts: Uint32Array;
10
+ export let bins: { binStart: number; binEnd: number }[];
11
+ export let type = "quantitative";
12
+ export let timeUnit = "";
13
+ export let width = 400;
14
+ export let height = 150;
15
+
16
+ let selection: null | any[] = null;
17
+ </script>
18
+
19
+ <div id="hist-container">
20
+ <div class="hist">
21
+ <TitleBar {title} {selection} />
22
+ <ContinuousHistogram
23
+ on:mouseenter
24
+ on:mouseleave
25
+ on:mousedown
26
+ on:mouseup
27
+ {dimLabel}
28
+ state={{ bin: bins, filter: filteredCounts, total: totalCounts }}
29
+ on:select
30
+ {type}
31
+ {timeUnit}
32
+ {width}
33
+ {height}
34
+ />
35
+ </div>
36
+ </div>
37
+
38
+ <style>
39
+ .hist {
40
+ display: inline-block;
41
+ }
42
+ #hist-container {
43
+ display: inline-block;
44
+ }
45
+ </style>
src/components/Legend.svelte ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import * as d3 from "d3";
3
+
4
+ export let data: ArrayLike<number>;
5
+ export let numberToColor: (t: number) => string;
6
+ export let width: number;
7
+ export let height: number;
8
+ export let title = "Density Legend";
9
+
10
+ let min: number, max: number;
11
+ let legendG: SVGGElement;
12
+
13
+ $: [min, max] = d3.extent(data);
14
+ $: scale = d3.scaleLinear().domain([0, max]).range([0, width]).nice();
15
+ $: axis = d3.axisBottom(scale).ticks(3);
16
+ $: {
17
+ if (legendG) {
18
+ d3.select(legendG).transition().call(axis);
19
+ }
20
+ }
21
+
22
+ const CONTINUOUS_COLOR_SCALE = interpolateToStringArray(
23
+ numberToColor,
24
+ 20,
25
+ 0.1
26
+ );
27
+
28
+ /**
29
+ * Takes a function that produces colors from numbers into a fixed sized array
30
+ *
31
+ * from [Zeno](https://github.com/zeno-ml/zeno/blob/main/frontend/src/instance-views/scatter-view/regl-scatter/colors.ts)
32
+ *
33
+ * @returns string array of hex colors
34
+ */
35
+ function interpolateToStringArray(
36
+ colorInterpolate: (x: number) => string,
37
+ length: number,
38
+ padLeft = 0,
39
+ padRight = 0
40
+ ) {
41
+ const colors: string[] = new Array(length);
42
+ const interval = 1 / (length - padLeft - padRight);
43
+ let inputValue = 0 + padLeft;
44
+ for (let i = 0; i < length; i++) {
45
+ // must be a normalized value
46
+ if (inputValue > 1) {
47
+ inputValue = 1;
48
+ } else if (inputValue < 0) {
49
+ inputValue = 0;
50
+ }
51
+
52
+ // from continuous function to string hex
53
+ const rgbString = colorInterpolate(inputValue);
54
+ colors[i] = d3.color(rgbString).hex();
55
+ inputValue += interval;
56
+ }
57
+
58
+ return colors;
59
+ }
60
+ </script>
61
+
62
+ <label for="#legend" class="label">{title}</label>
63
+ <div id="legend">
64
+ <div>
65
+ <svg {height} {width} style="border: 0.5px solid grey;">
66
+ {#each CONTINUOUS_COLOR_SCALE as color, i}
67
+ {@const spacing = width / CONTINUOUS_COLOR_SCALE.length}
68
+ {@const colorWidth =
69
+ (1 / CONTINUOUS_COLOR_SCALE.length) * width}
70
+ <rect
71
+ x={i * spacing}
72
+ y={0}
73
+ {height}
74
+ width={colorWidth}
75
+ fill={color}
76
+ />
77
+ {/each}
78
+ </svg>
79
+ </div>
80
+ <div>
81
+ <svg height={20} {width} style="overflow: visible;">
82
+ <g
83
+ bind:this={legendG}
84
+ transform="translate({0}, {0})"
85
+ fill="lightgrey"
86
+ clip-path="url(#clip)"
87
+ class="axis"
88
+ />
89
+ <defs>
90
+ <clipPath id="clip">
91
+ <rect x={-5} width={width + 25} height={20} fill="white" />
92
+ </clipPath>
93
+ </defs>
94
+ </svg>
95
+ </div>
96
+ </div>
97
+
98
+ <style>
99
+ .axis {
100
+ color: grey;
101
+ }
102
+ .label {
103
+ color: grey;
104
+ font-size: smaller;
105
+ }
106
+ </style>
src/components/StatePath.svelte ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+
4
+ export let d: string;
5
+ export let fill: string;
6
+ export let title = "";
7
+ export let stroke: string = "hsla(0, 0%, 0%, 0.075)";
8
+ export let strokeWidth = 0.75;
9
+ export let hoveringStroke = "black";
10
+ export let hoveringStrokeWidth = 1;
11
+ const dispatch = createEventDispatcher();
12
+ let isHovering = false;
13
+ </script>
14
+
15
+ <path
16
+ {d}
17
+ stroke={isHovering ? hoveringStroke : stroke}
18
+ {fill}
19
+ stroke-width={isHovering ? hoveringStrokeWidth : strokeWidth}
20
+ on:click
21
+ on:mouseenter={() => {
22
+ isHovering = true;
23
+ dispatch("mouseenter");
24
+ }}
25
+ on:mouseleave={() => {
26
+ isHovering = false;
27
+ dispatch("mouseleave");
28
+ }}
29
+ >
30
+ {#if title}
31
+ <title>{title}</title>
32
+ {/if}
33
+ </path>
src/components/TitleBar.svelte ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+
4
+ const dispatch = createEventDispatcher();
5
+
6
+ export let title: string = "";
7
+ export let selection: null | any[] = null;
8
+ </script>
9
+
10
+ <div class="top">
11
+ <div class="title">{title}</div>
12
+ {#if selection}
13
+ <div>
14
+ <span class="selection"
15
+ >[{selection}]<span />
16
+ <button
17
+ class="reset"
18
+ on:click={async () => {
19
+ dispatch("reset");
20
+ }}
21
+ >Reset
22
+ </button>
23
+ </span>
24
+ </div>
25
+ {/if}
26
+ </div>
27
+
28
+ <style>
29
+ .top {
30
+ display: flex;
31
+ justify-content: space-between;
32
+ }
33
+ .reset {
34
+ padding: 0.2em 1em;
35
+ font-size: 1em;
36
+ font-weight: 400;
37
+ font-family: inherit;
38
+ cursor: pointer;
39
+ transition: border-color 0.25s;
40
+ }
41
+ .selection {
42
+ color: lightgrey;
43
+ font-size: smaller;
44
+ }
45
+ .title {
46
+ font-family: "Exo", sans-serif;
47
+ font-size: 18px;
48
+ color: var(--primary-color);
49
+ }
50
+ </style>
src/components/TotalCount.svelte ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import VegaLite from "svelte-vega/src/VegaLite.svelte";
3
+ import type { VegaLiteSpec } from "svelte-vega";
4
+ import TitleBar from "./TitleBar.svelte";
5
+ import { primary } from "./colors";
6
+
7
+ export let filteredCount: number;
8
+ export let totalCount: number;
9
+ export let width = 500;
10
+ export let height = 50;
11
+ export let barColor = primary;
12
+ export let title = "Table Rows Selected";
13
+
14
+ $: spec = {
15
+ $schema: "https://vega.github.io/schema/vega-lite/v5.json",
16
+ data: {
17
+ name: "table",
18
+ },
19
+ width,
20
+ height,
21
+ title: null,
22
+ mark: { type: "bar" },
23
+ encoding: {
24
+ x: {
25
+ scale: { domain: [0, totalCount] },
26
+ type: "quantitative",
27
+ title: null,
28
+ field: "filteredCount",
29
+ axis: { tickCount: 5 },
30
+ },
31
+ color: { value: barColor },
32
+ },
33
+ } as VegaLiteSpec;
34
+ </script>
35
+
36
+ <TitleBar {title} />
37
+ <VegaLite
38
+ data={{ table: { filteredCount, totalCount } }}
39
+ {spec}
40
+ options={{ tooltip: true, actions: false, theme: "vox" }}
41
+ />
src/components/USMap.svelte ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import { states } from "./states";
4
+ import StatePath from "./StatePath.svelte";
5
+
6
+ const dispatch = createEventDispatcher();
7
+
8
+ export let stateToStyle: Map<string, { fill?: string; stroke?: string }> =
9
+ new Map([["CA", { fill: "red" }]]);
10
+ export let width = 500;
11
+ export let defaultFill = "hsla(0, 0%, 0%, 0.025)";
12
+ export let defaultStyle = {
13
+ stroke: "hsla(0, 0%, 0%, 0.075)",
14
+ fill: defaultFill,
15
+ };
16
+ export let selected = [];
17
+ let holdingShift = false;
18
+ </script>
19
+
20
+ <svelte:window
21
+ on:keyup={(e) => {
22
+ if (e.key === "Shift") {
23
+ holdingShift = false;
24
+ }
25
+ }}
26
+ on:keydown={(e) => {
27
+ if (e.key === "Shift") {
28
+ holdingShift = true;
29
+ }
30
+ }}
31
+ />
32
+
33
+ <svg
34
+ {width}
35
+ viewBox="0 0 468 280"
36
+ fill="none"
37
+ xmlns="http://www.w3.org/2000/svg"
38
+ on:mouseenter
39
+ on:mouseleave
40
+ >
41
+ <!-- TODO create a map -> from the d -->
42
+ <g id="usa" clip-path="url(#clip0_1_121)">
43
+ {#each states as state}
44
+ {@const style = stateToStyle.get(state.id) ?? defaultStyle}
45
+ <StatePath
46
+ title={state.id}
47
+ d={state.d}
48
+ fill={style.fill ?? defaultStyle.fill}
49
+ stroke={style.stroke ?? defaultStyle.stroke}
50
+ on:click={() => {
51
+ // if we hold shift, can select multiple or deselect multiple if they are already selected
52
+ const alreadySelected = selected.includes(state.id);
53
+ if (holdingShift) {
54
+ if (alreadySelected) {
55
+ selected = selected.filter((s) => s !== state.id);
56
+ } else {
57
+ // if not, add it to the selection!
58
+ selected.push(state.id);
59
+ selected = selected;
60
+ }
61
+ } else {
62
+ // if we don't hold shift, can only select and deselect one
63
+ if (alreadySelected && selected.length === 1) {
64
+ selected = [];
65
+ } else {
66
+ selected = [state.id];
67
+ }
68
+ }
69
+
70
+ dispatch("select", selected);
71
+ }}
72
+ />
73
+ {/each}
74
+ </g>
75
+ <defs>
76
+ <clipPath id="clip0_1_121">
77
+ <rect width="468" height="280" fill="white" />
78
+ </clipPath>
79
+ </defs>
80
+ </svg>
src/components/USMapVis.svelte ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import UsMap from "./USMap.svelte";
3
+ import TitleBar from "./TitleBar.svelte";
4
+ import * as d3 from "d3";
5
+ import { states } from "./states";
6
+ import { createEventDispatcher } from "svelte";
7
+ import type { View1DState } from "falcon-vis";
8
+ import Legend from "./Legend.svelte";
9
+ import { primary } from "./colors";
10
+
11
+ const dispatch = createEventDispatcher();
12
+
13
+ const primaryColorInterpolate = d3.interpolateRgbBasis([
14
+ "rgb(255,255,255)",
15
+ primary,
16
+ ]);
17
+
18
+ export let state: View1DState;
19
+ export let title: string = "";
20
+
21
+ export let numberToColor = (n: number) => primaryColorInterpolate(n);
22
+ export let width = 600;
23
+
24
+ let stateToStyle: Map<string, { fill?: string; stroke?: string }> = new Map(
25
+ states.map((state) => [state.id, { fill: "white" }])
26
+ );
27
+ let stateToStyleClone = structuredClone(stateToStyle);
28
+
29
+ function copyState(state: View1DState) {
30
+ stateToStyle = updateStateStyleMap(state);
31
+ stateToStyleClone = structuredClone(stateToStyle);
32
+ }
33
+ $: if (state) {
34
+ copyState(state);
35
+ }
36
+ function updateStateStyleMap(viewCounts: View1DState) {
37
+ const stateNames = viewCounts.bin;
38
+ const counts = viewCounts.filter;
39
+ let [_, maxCount] = d3.extent(counts);
40
+ if (maxCount <= 0) {
41
+ maxCount = 1;
42
+ }
43
+ for (let i = 0; i < stateNames.length; i++) {
44
+ const stateName = stateNames[i];
45
+ const count = counts[i];
46
+ const normalizedCount = count / maxCount;
47
+ const color = numberToColor(normalizedCount);
48
+ stateToStyle.set(stateName, {
49
+ ...stateToStyle.get(stateName),
50
+ fill: color,
51
+ });
52
+ }
53
+
54
+ return stateToStyle;
55
+ }
56
+
57
+ let selected = [];
58
+ /**
59
+ * @TODO fix this mess
60
+ */
61
+ async function selectMap(selected: string[]) {
62
+ if (selected.length > 0) {
63
+ stateToStyle = structuredClone(stateToStyleClone);
64
+ selected.forEach((state) => {
65
+ stateToStyle.set(state, {
66
+ ...stateToStyle.get(state),
67
+ stroke: "hsla(0, 0%, 0%, 0.5)",
68
+ });
69
+ });
70
+ stateToStyle = stateToStyle;
71
+ } else {
72
+ // reset it all to noew stroke
73
+ states.forEach((state) => {
74
+ stateToStyle.set(state.id, {
75
+ fill: stateToStyle.get(state.id).fill,
76
+ });
77
+ });
78
+ stateToStyle = stateToStyle;
79
+ }
80
+ }
81
+ </script>
82
+
83
+ <TitleBar
84
+ {title}
85
+ selection={selected.length > 0 ? selected : null}
86
+ on:reset={() => {
87
+ selected = [];
88
+ // I should remove all the selection
89
+ selectMap(selected);
90
+ dispatch("select", null);
91
+ }}
92
+ />
93
+ <UsMap
94
+ on:mouseenter
95
+ on:mouseleave
96
+ {width}
97
+ {stateToStyle}
98
+ on:select={async (e) => {
99
+ selected = e.detail;
100
+ if (e.detail.length > 0) {
101
+ selectMap(selected);
102
+ dispatch("select", selected);
103
+ } else {
104
+ stateToStyle = structuredClone(stateToStyleClone);
105
+ dispatch("select", null);
106
+ }
107
+ }}
108
+ />
109
+
110
+ <Legend
111
+ title="{title} Density"
112
+ data={state.filter}
113
+ {numberToColor}
114
+ width={200}
115
+ height={10}
116
+ />
117
+
118
+ <style>
119
+ /* put stuff here */
120
+ </style>
src/components/VLCategorical.svelte ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import type { VegaLiteSpec } from "svelte-vega";
4
+ import { VegaLite } from "svelte-vega";
5
+ import type { View } from "svelte-vega";
6
+
7
+ const dispatch = createEventDispatcher<{
8
+ select: any[] | null;
9
+ }>();
10
+
11
+ export let bins = [
12
+ { bin: "chicken", count: 200, filteredCount: 150 },
13
+ { bin: "dog", count: 300, filteredCount: 25 },
14
+ ];
15
+
16
+ export let title = "";
17
+ export let width = 400;
18
+ export let height = 125;
19
+ export let countLabel = "Count";
20
+ export let dimLabel = "";
21
+ export let labelColor = "hsla(0, 0%, 100%, 0.9)";
22
+ export let backgroundBarColor = "hsla(0, 0%, 100%, 0.5)";
23
+ export let foregroundBarColor = "hsla(172, 97%, 45%, 0.95)";
24
+ export let backgroundColor = "hsl(240,23%,9%)";
25
+ export let onlyFiltered = false;
26
+
27
+ $: data = {
28
+ table: bins,
29
+ };
30
+
31
+ $: spec = {
32
+ $schema: "https://vega.github.io/schema/vega-lite/v5.json",
33
+ description: "A categorical bar chart",
34
+ data: {
35
+ name: "table",
36
+ },
37
+ background: backgroundColor,
38
+ width: width,
39
+ height: height,
40
+ title: title,
41
+ layer: [
42
+ {
43
+ params: [
44
+ {
45
+ name: "select",
46
+ select: { type: "point", encodings: ["x"] },
47
+ },
48
+ ],
49
+ mark: { type: "bar", cursor: "pointer" },
50
+ encoding: {
51
+ x: {
52
+ field: "bin",
53
+ axis: {
54
+ title: dimLabel,
55
+ titleColor: labelColor,
56
+ labelColor: labelColor,
57
+ },
58
+ },
59
+ y: {
60
+ field: onlyFiltered ? "filteredCount" : "count",
61
+ type: "quantitative",
62
+ axis: {
63
+ title: countLabel,
64
+ titleColor: labelColor,
65
+ labelColor: labelColor,
66
+ tickCount: 3,
67
+ },
68
+ },
69
+ color: { value: backgroundBarColor },
70
+ stroke: {
71
+ condition: [
72
+ {
73
+ param: "select",
74
+ empty: false,
75
+ value: labelColor,
76
+ },
77
+ ],
78
+ value: "transparent",
79
+ },
80
+ strokeWidth: {
81
+ condition: [
82
+ {
83
+ param: "select",
84
+ empty: false,
85
+ value: 3,
86
+ },
87
+ ],
88
+ value: 0,
89
+ },
90
+ },
91
+ color: { value: foregroundBarColor },
92
+ },
93
+ {
94
+ mark: {
95
+ type: "bar",
96
+ },
97
+ encoding: {
98
+ size: {
99
+ legend: null,
100
+ },
101
+ x: {
102
+ field: "bin",
103
+ title: "",
104
+ },
105
+ y: {
106
+ field: "filteredCount",
107
+ type: "quantitative",
108
+ title: "",
109
+ },
110
+ color: { value: foregroundBarColor },
111
+ },
112
+ },
113
+ ],
114
+ } as VegaLiteSpec;
115
+
116
+ let view: View;
117
+ let runOnce = false;
118
+ $: if (view && !runOnce) {
119
+ view.addSignalListener("select", (...s) => {
120
+ let out: any[] | null;
121
+ if (s[1] && "bin" in s[1]) {
122
+ out = s[1]["bin"];
123
+ } else {
124
+ out = null;
125
+ }
126
+ dispatch("select", out);
127
+ });
128
+ runOnce = true;
129
+ }
130
+ </script>
131
+
132
+ <div
133
+ on:click
134
+ on:mouseup
135
+ on:mousedown
136
+ on:mouseenter
137
+ on:mouseleave
138
+ class="container"
139
+ >
140
+ <VegaLite
141
+ bind:view
142
+ {data}
143
+ {spec}
144
+ options={{ tooltip: true, actions: false, theme: "vox" }}
145
+ />
146
+ </div>
147
+
148
+ <style>
149
+ .container {
150
+ display: inline-block;
151
+ }
152
+ </style>
src/components/VLContinuous.svelte ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { createEventDispatcher } from "svelte";
3
+ import type { VegaLiteSpec } from "svelte-vega";
4
+ import { VegaLite } from "svelte-vega";
5
+ import type { View } from "svelte-vega";
6
+ import { primary } from "./colors";
7
+
8
+ const dispatch = createEventDispatcher<{
9
+ select: [number, number] | null;
10
+ }>();
11
+
12
+ export let bins = [
13
+ { bin: [0, 10], count: 200, filteredCount: 150 },
14
+ { bin: [10, 20], count: 300, filteredCount: 25 },
15
+ { bin: [20, 30], count: 72, filteredCount: 12 },
16
+ ];
17
+
18
+ export let title = "";
19
+ export let width = 400;
20
+ export let height = 125;
21
+ export let countLabel = "Count";
22
+ export let dimLabel = "";
23
+ export let labelColor = "black";
24
+ export let backgroundBarColor = "hsla(0, 0%, 0%, 0.07)";
25
+ export let foregroundBarColor = primary;
26
+ export let backgroundColor = "white";
27
+ export let onlyFiltered = false;
28
+ export let type: "quantitative" | "temporal" = "quantitative";
29
+ export let timeUnit = "";
30
+
31
+ $: data = {
32
+ table: bins,
33
+ };
34
+
35
+ $: spec = {
36
+ $schema: "https://vega.github.io/schema/vega-lite/v5.json",
37
+ description: "A simple bar chart with embedded data.",
38
+ data: {
39
+ name: "table",
40
+ },
41
+ background: "transparent",
42
+ width: width,
43
+ height: height,
44
+ title: { text: title, anchor: "start" },
45
+ layer: [
46
+ {
47
+ params: [
48
+ {
49
+ name: "select",
50
+ select: {
51
+ type: "interval",
52
+ encodings: ["x"],
53
+ },
54
+ },
55
+ ],
56
+ mark: { type: "bar", cursor: "col-resize" },
57
+ encoding: {
58
+ x: {
59
+ ...(timeUnit ? { timeUnit } : {}),
60
+ // timeUnit: "utcmonthdate"
61
+ field: "bin[0]",
62
+ type,
63
+ bin: { binned: true },
64
+ axis: {
65
+ title: dimLabel,
66
+ titleColor: labelColor,
67
+ labelColor: labelColor,
68
+ },
69
+ },
70
+ x2: { field: "bin[1]" },
71
+ y: {
72
+ field: onlyFiltered ? "filteredCount" : "count",
73
+ type: "quantitative",
74
+ axis: {
75
+ title: "Count",
76
+ titleColor: labelColor,
77
+ tickCount: 3,
78
+ labelColor: labelColor,
79
+ },
80
+ },
81
+ color: { value: backgroundBarColor },
82
+ },
83
+ },
84
+ {
85
+ mark: {
86
+ type: "bar",
87
+ },
88
+ encoding: {
89
+ size: {
90
+ legend: null,
91
+ },
92
+ x: {
93
+ ...(timeUnit ? { timeUnit } : {}),
94
+ field: "bin[0]",
95
+ type,
96
+ bin: { binned: true },
97
+ title: "",
98
+ },
99
+ x2: { field: "bin[1]" },
100
+ y: {
101
+ field: "filteredCount",
102
+ type: "quantitative",
103
+ },
104
+ color: { value: foregroundBarColor },
105
+ },
106
+ },
107
+ ],
108
+ } as VegaLiteSpec;
109
+
110
+ let view: View;
111
+ let runOnce = false;
112
+ $: if (view && !runOnce) {
113
+ view.addSignalListener("select", (...s) => {
114
+ dispatch("select", s[1][`bin\\.0`] ?? null);
115
+ });
116
+ runOnce = true;
117
+ }
118
+ </script>
119
+
120
+ <div
121
+ on:click
122
+ on:mousedown
123
+ on:mouseup
124
+ on:mouseenter
125
+ on:mouseleave
126
+ class="container"
127
+ >
128
+ <VegaLite
129
+ bind:view
130
+ {data}
131
+ {spec}
132
+ options={{ tooltip: true, actions: false, theme: "vox" }}
133
+ />
134
+ </div>
135
+
136
+ <style>
137
+ .container {
138
+ display: inline-block;
139
+ }
140
+ </style>
src/components/colors.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export const primary = "hsl(171,69%,47%)";
src/components/states.ts ADDED
The diff for this file is too large to render. See raw diff
 
src/main.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import './app.css'
2
+ import App from './App.svelte'
3
+
4
+ const app = new App({
5
+ target: document.getElementById('app'),
6
+ })
7
+
8
+ export default app
src/vite-env.d.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ /// <reference types="svelte" />
2
+ /// <reference types="vite/client" />
svelte.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
2
+
3
+ export default {
4
+ // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5
+ // for more information about preprocessors
6
+ preprocess: vitePreprocess(),
7
+ }
tsconfig.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "@tsconfig/svelte/tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ESNext",
5
+ "useDefineForClassFields": true,
6
+ "module": "ESNext",
7
+ "resolveJsonModule": true,
8
+ /**
9
+ * Typecheck JS in `.svelte` and `.js` files by default.
10
+ * Disable checkJs if you'd like to use dynamic types in JS.
11
+ * Note that setting allowJs false does not prevent the use
12
+ * of JS in `.svelte` files.
13
+ */
14
+ "allowJs": true,
15
+ "checkJs": true,
16
+ "isolatedModules": true
17
+ },
18
+ "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
19
+ "references": [{ "path": "./tsconfig.node.json" }]
20
+ }
tsconfig.node.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node"
6
+ },
7
+ "include": ["vite.config.ts"]
8
+ }
vite.config.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import { svelte } from "@sveltejs/vite-plugin-svelte";
3
+
4
+ // https://vitejs.dev/config/
5
+ export default defineConfig({
6
+ plugins: [svelte()],
7
+ optimizeDeps: {
8
+ include: [
9
+ "fast-deep-equal",
10
+ "clone",
11
+ "semver",
12
+ "json-stringify-pretty-compact",
13
+ "fast-json-stable-stringify",
14
+ ],
15
+ },
16
+ });