diff --git a/README.md b/README.md index 90386156..65208f17 100644 --- a/README.md +++ b/README.md @@ -54,15 +54,15 @@ To raise the sliders, you need to click on one of the examples that are displaye - [ ] Make keyboard short cut (option+up/down) for the z-direction - [ ] Polar stuff - [ ] Chore: Extract some common types into a top level location (like types.ts type thing) -- [x] Examples: A (default) example showing annotated math stuff -- [ ] Examples: SVG Upload -- [ ] Examples: Vega/Vega-Lite Specification -- [ ] Examples: Examples held as assets somewhere that are downloaded rather than components (for consistency) -- [ ] Examples: higher card. vis examples - [?] Rearrange some of the colors in the color area eg make rg on xy and b on z etc - [ ] Insert color theory options, eg insert opposing, inserting analogous color, etc, mine from the adobe picker - [ ] Labels, tooltips, etc - [ ] Nice to have: Rest of basic geometry manipulations: flip (horizontal, vertical), scale, Distribute radially +- [x] Examples: A (default) example showing annotated math stuff +- [x] Examples: SVG Upload +- [x] Examples: Vega/Vega-Lite Specification +- [x] Examples: Examples held as assets somewhere that are downloaded rather than components (for consistency) +- [x] Examples: higher card. vis examples - [x] Black and white simulation - [x] Chore: clean up the color store, many unnecessary methods. - [x] Bug: changing the color frame fucks everything up???? (particularly for HSV/HSL) diff --git a/public/HolyGrail.svg b/public/examples/HolyGrail.svg similarity index 99% rename from public/HolyGrail.svg rename to public/examples/HolyGrail.svg index 6e713795..e6d6cf93 100644 --- a/public/HolyGrail.svg +++ b/public/examples/HolyGrail.svg @@ -1,6 +1,6 @@ - + diff --git a/public/examples/area-chart.json b/public/examples/area-chart.json new file mode 100644 index 00000000..ea869664 --- /dev/null +++ b/public/examples/area-chart.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "width": 250, + "height": 200, + "data": { + "url": "data/penguins.json" + }, + "mark": { + "type": "area", + "opacity": 0.5 + }, + "transform": [ + { + "density": "Body Mass (g)", + "groupby": [ + "Species" + ], + "extent": [ + 2500, + 6500 + ] + } + ], + "encoding": { + "x": { + "field": "value", + "type": "quantitative", + "title": "Body Mass (g)" + }, + "y": { + "field": "density", + "type": "quantitative", + "stack": null + }, + "color": { + "field": "Species", + "type": "nominal" + } + } +} \ No newline at end of file diff --git a/public/fourier.svg b/public/examples/fourier.svg similarity index 100% rename from public/fourier.svg rename to public/examples/fourier.svg diff --git a/public/examples/grouped-bar-chart.json b/public/examples/grouped-bar-chart.json new file mode 100644 index 00000000..1b8b070c --- /dev/null +++ b/public/examples/grouped-bar-chart.json @@ -0,0 +1,70 @@ +{ + "height": 150, + "width": 150, + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "values": [ + { + "category": "A", + "group": "x", + "value": 0.1 + }, + { + "category": "A", + "group": "y", + "value": 0.6 + }, + { + "category": "A", + "group": "z", + "value": 0.9 + }, + { + "category": "B", + "group": "x", + "value": 0.7 + }, + { + "category": "B", + "group": "y", + "value": 0.2 + }, + { + "category": "B", + "group": "z", + "value": 1.1 + }, + { + "category": "C", + "group": "x", + "value": 0.6 + }, + { + "category": "C", + "group": "y", + "value": 0.1 + }, + { + "category": "C", + "group": "z", + "value": 0.2 + } + ] + }, + "mark": "bar", + "encoding": { + "x": { + "field": "category" + }, + "y": { + "field": "value", + "type": "quantitative" + }, + "xOffset": { + "field": "group" + }, + "color": { + "field": "group" + } + } +} \ No newline at end of file diff --git a/public/examples/heatmap.json b/public/examples/heatmap.json new file mode 100644 index 00000000..f814a0a2 --- /dev/null +++ b/public/examples/heatmap.json @@ -0,0 +1,83 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "values": [ + { + "x": 0, + "y": 0, + "value": -10 + }, + { + "x": 1, + "y": 0, + "value": -5 + }, + { + "x": 2, + "y": 0, + "value": 0 + }, + { + "x": 3, + "y": 0, + "value": 5 + }, + { + "x": 0, + "y": 1, + "value": -5 + }, + { + "x": 1, + "y": 1, + "value": 0 + }, + { + "x": 2, + "y": 1, + "value": 5 + }, + { + "x": 3, + "y": 1, + "value": 10 + }, + { + "x": 0, + "y": 2, + "value": 0 + }, + { + "x": 1, + "y": 2, + "value": 5 + }, + { + "x": 2, + "y": 2, + "value": 10 + }, + { + "x": 3, + "y": 2, + "value": 15 + } + ] + }, + "mark": "rect", + "encoding": { + "x": { + "field": "x", + "type": "ordinal" + }, + "y": { + "field": "y", + "type": "ordinal" + }, + "color": { + "field": "value", + "type": "ordinal", + "bin": true + } + } +} \ No newline at end of file diff --git a/public/examples/illinois-map.json b/public/examples/illinois-map.json new file mode 100644 index 00000000..7074f301 --- /dev/null +++ b/public/examples/illinois-map.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "width": 250, + "height": 250, + "autosize": "none", + "data": [ + { + "name": "counties", + "url": "data/us-10m.json", + "format": { + "type": "topojson", + "feature": "counties" + }, + "transform": [ + { + "type": "formula", + "expr": "toNumber(datum.id)", + "as": "id2" + }, + { + "type": "filter", + "expr": "datum.id2 > 17000 && datum.id2 < 18000" + } + ] + } + ], + "projections": [ + { + "name": "projection", + "type": "albersUsa", + "translate": [ + -100, + 180 + ], + "scale": 2500 + } + ], + "scales": [ + { + "name": "color", + "type": "ordinal", + "domain": { + "data": "counties", + "field": "id2" + }, + "range": "category" + } + ], + "marks": [ + { + "type": "shape", + "from": { + "data": "counties" + }, + "encode": { + "update": { + "fill": { + "scale": "color", + "field": "id2" + } + } + }, + "transform": [ + { + "type": "geoshape", + "projection": "projection" + } + ] + } + ] +} \ No newline at end of file diff --git a/public/examples/mondrian.svg b/public/examples/mondrian.svg new file mode 100644 index 00000000..09672d5d --- /dev/null +++ b/public/examples/mondrian.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/r2.svg b/public/examples/r2.svg similarity index 100% rename from public/r2.svg rename to public/examples/r2.svg diff --git a/public/examples/scatterplot-ordinal.json b/public/examples/scatterplot-ordinal.json new file mode 100644 index 00000000..0df4a199 --- /dev/null +++ b/public/examples/scatterplot-ordinal.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "url": "data/penguins.json" + }, + "mark": "point", + "encoding": { + "x": { + "field": "Flipper Length (mm)", + "type": "quantitative", + "scale": { + "zero": false + } + }, + "y": { + "field": "Body Mass (g)", + "type": "quantitative", + "scale": { + "zero": false + } + }, + "color": { + "field": "Flipper Length (mm)", + "type": "ordinal", + "legend": null + }, + "shape": { + "field": "Species", + "type": "nominal" + } + } +} \ No newline at end of file diff --git a/public/examples/scatterplot.json b/public/examples/scatterplot.json new file mode 100644 index 00000000..9cbccc77 --- /dev/null +++ b/public/examples/scatterplot.json @@ -0,0 +1,34 @@ +{ + "height": 150, + "width": 150, + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "A scatterplot showing body mass and flipper lengths of penguins.", + "data": { + "url": "data/penguins.json" + }, + "mark": "point", + "encoding": { + "x": { + "field": "Flipper Length (mm)", + "type": "quantitative", + "scale": { + "zero": false + } + }, + "y": { + "field": "Body Mass (g)", + "type": "quantitative", + "scale": { + "zero": false + } + }, + "color": { + "field": "Species", + "type": "nominal" + }, + "shape": { + "field": "Species", + "type": "nominal" + } + } +} \ No newline at end of file diff --git a/public/examples/stacked-area-chart.json b/public/examples/stacked-area-chart.json new file mode 100644 index 00000000..380f7c9f --- /dev/null +++ b/public/examples/stacked-area-chart.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "url": "data/barley.json" + }, + "mark": "bar", + "encoding": { + "x": { + "field": "yield", + "type": "quantitative", + "aggregate": "sum" + }, + "y": { + "field": "variety", + "type": "nominal" + }, + "color": { + "field": "site", + "type": "nominal" + } + } +} \ No newline at end of file diff --git a/public/vis-logo.svg b/public/examples/vis-logo.svg similarity index 100% rename from public/vis-logo.svg rename to public/examples/vis-logo.svg diff --git a/public/mondrian.svg b/public/mondrian.svg deleted file mode 100644 index 77507b7f..00000000 --- a/public/mondrian.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - diff --git a/src/content-modules/Examples.svelte b/src/content-modules/Examples.svelte index 221e8e0e..f9f88794 100644 --- a/src/content-modules/Examples.svelte +++ b/src/content-modules/Examples.svelte @@ -2,9 +2,12 @@ import { colorFromString } from "../lib/Color"; import type { Palette } from "../stores/color-store"; import chroma from "chroma-js"; - import exampleStore from "../stores/example-store"; + import exampleStore, { + DEMOS, + detectColorsInSvgString, + modifySVGForExampleStore, + } from "../stores/example-store"; import colorStore from "../stores/color-store"; - import { charts, idxToKey } from "../lib/charts"; import Vega from "../components/Vega.svelte"; import Modal from "../components/Modal.svelte"; import { buttonStyle } from "../lib/styles"; @@ -14,35 +17,13 @@ import Example from "../components/Example.svelte"; import Swatches from "../content-modules/Swatches.svelte"; - let modalState: "closed" | "input-svg" | "edit-colors" = "closed"; + let modalState: "closed" | "input-svg" | "input-vega" | "edit-colors" = + "closed"; let modifyingExample: number | false = false; $: bg = $colorStore.currentPal.background; $: colorSpace = $colorStore.currentPal.colorSpace; let value = ""; - function detectColorsInSvgString(svgString: string) { - const colors = new Set(); - // match hex or rgb(255, 255, 255) - const regex = /#(?:[0-9a-fA-F]{3}){1,2}|rgb\((?:\d{1,3},\s*){2}\d{1,3}\)/g; - - let match; - while ((match = regex.exec(svgString))) { - colors.add(match[0]); - } - return Array.from(colors); - } - - function modifySVGForExampleStore( - svgString: string, - targetedColors: string[] - ) { - let svg = svgString; - targetedColors.forEach((color, idx) => { - svg = svg.replace(new RegExp(color, "g"), idxToKey(idx)); - }); - return svg; - } - $: detectedColors = [] as string[]; $: sections = $exampleStore.sections as any; function onToggle(group: string) { @@ -60,17 +41,49 @@ }; colorStore.createNewPal(newPal); } - const DEMOS = [ - { title: "Mondrian", filename: "./mondrian.svg" }, - { title: "Annotated R2", filename: "./r2.svg" }, - { title: "Annotated Fourier", filename: "./fourier.svg" }, - { title: "VIS Logo", filename: "./vis-logo.svg" }, - { title: "Holy Grail Layout", filename: "./HolyGrail.svg" }, - ]; + + let validJSON = false; + $: { + if (modalState === "input-vega") { + try { + JSON.parse(value); + validJSON = true; + } catch (e) { + validJSON = false; + } + } + } + function clickExample(example: { svg?: string; vega?: string }, idx: number) { + if (example.svg) { + value = example.svg; + modalState = "input-svg"; + } + if (example.vega) { + value = example.vega; + modalState = "input-vega"; + } + modifyingExample = idx; + } + + async function fileUpload(e: any) { + const file = e.target.files[0]; + const text = await file.text(); + value = text; + } + + $: examples = $exampleStore.examples.filter((x: any) => { + if (sections.svg && x?.svg) { + return true; + } + if (sections.vega && x.vega) { + return true; + } + return false; + }) as any;
- {#each Object.keys($exampleStore.sections) as group} + {#each Object.keys(sections) as group}
Add Example +
- {#if $exampleStore.sections["custom"]} - {#each $exampleStore.examples as example, idx} -
-
- - -
- - -
-
- -
-
-
-
- {/each} - {/if} {#if $exampleStore.sections["swatches"]} {/if} - {#each charts as { chart, group }} - {#if sections[group]} -
- + + {#each examples as example, idx} +
+
+ {#if example.svg} + + {/if} + {#if example.vega} + + {/if} + +
+ + +
+
+ +
+
- {/if} +
{/each}
{#if modalState !== "closed"} @@ -153,21 +165,38 @@
Add an Example
+
+ {#each ["svg", "vega (or vega-lite)"] as mode} + + {/each} +
- Demos: - {#if modalState === "input-svg"} - {#each DEMOS as demo} + {#if modalState === "input-svg" || modalState === "input-vega"} + Demos: + {#each DEMOS.filter((demo) => { + return modalState === "input-svg" ? demo.type === "svg" : demo.type === "vega"; + }) as demo} {/if} + {#if modalState === "input-vega"} + + {/if}
{/if} diff --git a/src/content-modules/Swatches.svelte b/src/content-modules/Swatches.svelte index 35a760cc..18d76bf6 100644 --- a/src/content-modules/Swatches.svelte +++ b/src/content-modules/Swatches.svelte @@ -92,14 +92,22 @@ {/each}
{#each colors as color, i} -
{ + const isMeta = e.metaKey || e.shiftKey; + if (isMeta) { + focusStore.toggleColor(i); + } else { + focusStore.setColors(focusSet.has(i) ? [] : [i]); + } + }} > {color.toHex()} -
+ {/each}
diff --git a/src/lib/charts.ts b/src/lib/charts.ts index c9d49aae..3e507358 100644 --- a/src/lib/charts.ts +++ b/src/lib/charts.ts @@ -109,212 +109,137 @@ export function buildTheme(pal: Palette): any { }; } -const groupedBarChart = (_pal: Palette) => ({ - height: 150, - width: 150, - $schema: "https://vega.github.io/schema/vega-lite/v5.json", - data: { - values: [ - { category: "A", group: "x", value: 0.1 }, - { category: "A", group: "y", value: 0.6 }, - { category: "A", group: "z", value: 0.9 }, - { category: "B", group: "x", value: 0.7 }, - { category: "B", group: "y", value: 0.2 }, - { category: "B", group: "z", value: 1.1 }, - { category: "C", group: "x", value: 0.6 }, - { category: "C", group: "y", value: 0.1 }, - { category: "C", group: "z", value: 0.2 }, - ], - }, - mark: "bar", - encoding: { - x: { field: "category" }, - y: { field: "value", type: "quantitative" }, - xOffset: { field: "group" }, - color: { field: "group" }, - }, -}); -const scatterPlot = (_pal: Palette) => ({ - height: 150, - width: 150, - $schema: "https://vega.github.io/schema/vega-lite/v5.json", - description: - "A scatterplot showing body mass and flipper lengths of penguins.", - data: { - url: !location.href.includes("localhost") - ? "https://vega.github.io/editor/data/penguins.json" - : "data/penguins.json", - }, - mark: "point", - encoding: { - x: { - field: "Flipper Length (mm)", - type: "quantitative", - scale: { zero: false }, - }, - y: { - field: "Body Mass (g)", - type: "quantitative", - scale: { zero: false }, - }, - color: { field: "Species", type: "nominal" }, - shape: { field: "Species", type: "nominal" }, - }, -}); - -const map = (pal: Palette) => ({ - $schema: "https://vega.github.io/schema/vega/v5.json", - width: 250, - height: 250, - autosize: "none", - data: [ - { - name: "counties", - url: !location.href.includes("localhost") - ? "https://vega.github.io/editor/data/us-10m.json" - : "data/us-10m.json", - format: { type: "topojson", feature: "counties" }, - transform: [ - { - type: "formula", - expr: `datum.id - (parseInt(datum.id/${pal.colors.length + 5}) * ${ - pal.colors.length - })`, - as: "mod", - }, - ], - }, - ], - projections: [ - { name: "projection", type: "albersUsa", translate: [0, 400], scale: 2900 }, - ], +// const groupedBarChart = () => (); +// const scatterPlot = () => (); - scales: [ - { - name: "color", - type: "ordinal", - domain: { data: "counties", field: "mod" }, - range: pal.colors.map((_x, idx) => idxToKey(idx)), - }, - ], - - marks: [ - { - type: "shape", - from: { data: "counties" }, - encode: { - update: { fill: { scale: "color", field: "mod" } }, - }, - transform: [{ type: "geoshape", projection: "projection" }], - }, - ], -}); +// const map = () => (); -const areaChart = (pal: Palette) => ({ - $schema: "https://vega.github.io/schema/vega-lite/v5.json", - width: 250, - height: 200, - data: { - url: !location.href.includes("localhost") - ? "https://vega.github.io/editor/data/penguins.json" - : "data/penguins.json", - }, - mark: { type: "area", opacity: 0.5 }, - transform: [ - { density: "Body Mass (g)", groupby: ["Species"], extent: [2500, 6500] }, - ], - encoding: { - x: { field: "value", type: "quantitative", title: "Body Mass (g)" }, - y: { field: "density", type: "quantitative", stack: null }, - color: { field: "Species", type: "nominal" }, - }, -}); +// const areaChart = () => (); -const stackedAreaChart = (pal: Palette) => ({ - $schema: "https://vega.github.io/schema/vega-lite/v5.json", - data: { - url: !location.href.includes("localhost") - ? "https://vega.github.io/editor/data/barley.json" - : "data/barley.json", - }, - mark: "bar", - encoding: { - x: { field: "yield", type: "quantitative", aggregate: "sum" }, - y: { field: "variety", type: "nominal" }, - color: { field: "site", type: "nominal" }, - }, -}); +// const stackedAreaChart = () => (); +// url: !location.href.includes("localhost") +// ? "https://vega.github.io/editor/data/penguins.json" +// : "data/penguins.json", -const scatterPlotOrdinal = (_pal: Palette) => ({ - $schema: "https://vega.github.io/schema/vega-lite/v5.json", - data: { - url: !location.href.includes("localhost") - ? "https://vega.github.io/editor/data/penguins.json" - : "data/penguins.json", - }, - mark: "point", - encoding: { - x: { - field: "Flipper Length (mm)", - type: "quantitative", - scale: { zero: false }, - }, - y: { - field: "Body Mass (g)", - type: "quantitative", - scale: { zero: false }, - }, - color: { field: "Flipper Length (mm)", type: "ordinal", legend: null }, - shape: { field: "Species", type: "nominal" }, - }, -}); +// const scatterPlotOrdinal = () => (); -const heatmap = (_pal: Palette) => ({ - $schema: "https://vega.github.io/schema/vega-lite/v5.json", - data: { - values: [ - { x: 0, y: 0, value: -10 }, - { x: 1, y: 0, value: -5 }, - { x: 2, y: 0, value: 0 }, - { x: 3, y: 0, value: 5 }, - { x: 0, y: 1, value: -5 }, - { x: 1, y: 1, value: 0 }, - { x: 2, y: 1, value: 5 }, - { x: 3, y: 1, value: 10 }, - { x: 0, y: 2, value: 0 }, - { x: 1, y: 2, value: 5 }, - { x: 2, y: 2, value: 10 }, - { x: 3, y: 2, value: 15 }, - ], - }, - mark: "rect", - encoding: { - x: { field: "x", type: "ordinal" }, - y: { field: "y", type: "ordinal" }, - color: { field: "value", type: "ordinal", bin: true }, - }, -}); +// const heatmap = () => (); export const charts = [ // groupedBarChart, // // { group: "categorical", chart: scatterPlot }, - { group: "categorical", chart: map }, - { group: "categorical", chart: areaChart }, - { group: "categorical", chart: stackedAreaChart }, - { group: "ordinal", chart: scatterPlotOrdinal }, - { group: "ordinal", chart: heatmap }, + // { group: "categorical", chart: map }, + // { group: "categorical", chart: areaChart }, + // { group: "categorical", chart: stackedAreaChart }, + // { group: "ordinal", chart: scatterPlotOrdinal }, + // { group: "ordinal", chart: heatmap }, +]; + +const vegaDatasets = [ + "7zip.png", + "airports.csv", + "annual-precip.json", + "anscombe.json", + "barley.json", + "birdstrikes.csv", + "budget.json", + "budgets.json", + "burtin.json", + "cars.json", + "co2-concentration.csv", + "countries.json", + "crimea.json", + "disasters.csv", + "driving.json", + "earthquakes.json", + "ffox.png", + "flare-dependencies.json", + "flare.json", + "flights-10k.json", + "flights-200k.arrow", + "flights-200k.json", + "flights-20k.json", + "flights-2k.json", + "flights-3m.csv", + "flights-5k.json", + "flights-airport.csv", + "football.json", + "gapminder-health-income.csv", + "gapminder.json", + "gimp.png", + "github.csv", + "income.json", + "iowa-electricity.csv", + "jobs.json", + "la-riots.csv", + "londonBoroughs.json", + "londonCentroids.json", + "londonTubeLines.json", + "lookup_groups.csv", + "lookup_people.csv", + "miserables.json", + "monarchs.json", + "movies.json", + "normal-2d.json", + "obesity.json", + "ohlc.json", + "penguins.json", + "platformer-terrain.json", + "points.json", + "political-contributions.json", + "population.json", + "population_engineers_hurricanes.csv", + "seattle-weather-hourly-normals.csv", + "seattle-weather.csv", + "sp500-2000.csv", + "sp500.csv", + "stocks.csv", + "udistrict.json", + "unemployment-across-industries.json", + "unemployment.tsv", + "uniform-2d.json", + "us-10m.json", + "us-employment.csv", + "us-state-capitals.json", + "volcano.json", + "weather.csv", + "weather.json", + "wheat.json", + "windvectors.csv", + "world-110m.json", + "zipcodes.csv", ]; const results: Record = {}; -export async function getSVG(localSpec: any, pal: Palette) { +export async function getSVG(localSpec: string, pal: Palette) { const newKey = pal.colors.length + pal.background.toHex() + JSON.stringify(localSpec); if (results[newKey]) return results[newKey]; const theme = buildTheme(pal); - let spec = localSpec; + let spec: any; + try { + spec = JSON.parse(localSpec); + } catch (e) { + console.error(e, localSpec); + return ""; + } if (spec.$schema.includes("vega-lite")) { spec = vegaLite.compile(spec, { config: theme }).spec; } + // // url: !location.href.includes("localhost") + // ? "https://vega.github.io/editor/data/penguins.json" + // : "data/penguins.json", + const matchedDataset = vegaDatasets.find((x) => spec.data?.url?.includes(x)); + if (matchedDataset) { + const isLocal = location.href.includes("localhost"); + if (isLocal) { + spec.data.url = `data/${matchedDataset}`; + } + if (!isLocal && spec.data.url === `data/${matchedDataset}`) { + spec.data.url = `https://vega.github.io/editor/data/${matchedDataset}`; + } + } + const runtime = vega.parse(spec, theme); const view = await new vega.View(runtime, { renderer: "svg" }).runAsync(); return await view.toSVG().then((x) => { diff --git a/src/stores/example-store.ts b/src/stores/example-store.ts index 68f9a910..ef29bd86 100644 --- a/src/stores/example-store.ts +++ b/src/stores/example-store.ts @@ -1,16 +1,15 @@ import { writable } from "svelte/store"; import * as idb from "idb-keyval"; +import { idxToKey } from "../lib/charts"; -type Example = { svg: string; numColors: number }; +type Example = { svg: string; numColors: number } | { vega: string }; interface StoreData { examples: Example[]; sections: typeof InitialSections; } const InitialSections = { - pages: true, - ordinal: true, - categorical: true, - custom: true, + svg: true, + vega: true, swatches: true, }; const InitialStore: StoreData = { @@ -18,6 +17,91 @@ const InitialStore: StoreData = { sections: InitialSections, }; +export const DEMOS = [ + { type: "svg", title: "Holy Grail", filename: "./examples/HolyGrail.svg" }, + { type: "svg", title: "Fourier", filename: "./examples/fourier.svg" }, + { type: "svg", title: "Mondrian", filename: "./examples/mondrian.svg" }, + { type: "svg", title: "R Squared", filename: "./examples/r2.svg" }, + { type: "svg", title: "Vis Logo", filename: "./examples/vis-logo.svg" }, + { + type: "vega", + title: "Area Chart", + filename: "./examples/area-chart.json", + }, + { + type: "vega", + title: "Grouped Bar Chart", + filename: "./examples/grouped-bar-chart.json", + }, + { type: "vega", title: "Heatmap", filename: "./examples/heatmap.json" }, + { type: "vega", title: "Map", filename: "./examples/illinois-map.json" }, + { + type: "vega", + title: "Scatterplot Ordinal", + filename: "./examples/scatterplot-ordinal.json", + }, + { + type: "vega", + title: "Scatterplot", + filename: "./examples/scatterplot.json", + }, + { + type: "vega", + title: "Stacked Area Chart", + filename: "./examples/stacked-area-chart.json", + }, +]; + +export function modifySVGForExampleStore( + svgString: string, + targetedColors: string[] +) { + let svg = svgString; + targetedColors.forEach((color, idx) => { + svg = svg.split(color).join(idxToKey(idx)); + }); + // remove anything like clip-path="url()" + svg = svg.replace(/clip-path="url\(.*?\)"/g, ""); + // remove anything like + svg = svg.replace(/<\?xml.*?>/g, ""); + return svg; +} + +export function detectColorsInSvgString(svgString: string) { + const colors = new Set(); + // match hex or rgb(255, 255, 255) + const regex = /#(?:[0-9a-fA-F]{3}){1,2}|rgb\((?:\d{1,3},\s*){2}\d{1,3}\)/g; + + let match; + while ((match = regex.exec(svgString))) { + colors.add(match[0]); + } + return Array.from(colors); +} + +async function buildAllExamples() { + const builtExamples = []; + for (const demo of DEMOS) { + try { + const text = await fetch(demo.filename).then((x) => x.text()); + const example = {} as any; + if (demo.type === "vega") { + example.vega = text; + } else { + const colors = detectColorsInSvgString(text); + store.addExample(example); + example.svg = modifySVGForExampleStore(text, colors); + example.numColors = colors.length; + } + builtExamples.push(example); + } catch (e) { + console.error("Failed to load example", demo, e); + continue; + } + } + return builtExamples; +} + const storeName = "color-pal-examples"; function createStore() { let storeData: StoreData = JSON.parse(JSON.stringify(InitialStore)); @@ -25,12 +109,21 @@ function createStore() { const { subscribe, set, update } = writable(storeData); idb.get(storeName).then((x) => { if (x) { + const savedSections = Object.fromEntries( + Object.entries(InitialStore.sections).map(([key, value]) => [ + key, + (x.sections || {})[key] || value, + ]) + ); const newStore = { - ...InitialStore, - ...x, - sections: { ...InitialSections, ...(x.sections || {}) }, + examples: x.examples || [], + sections: savedSections, }; - set(newStore); + set(newStore as StoreData); + } else { + buildAllExamples().then((examples) => { + set({ ...InitialStore, examples }); + }); } }); const persistUpdate = (updateFunc: (old: StoreData) => StoreData) => @@ -64,6 +157,10 @@ function createStore() { persistUpdate((old) => { return { ...old, examples: [...old.examples, example] }; }), + restoreDefaultExamples: async () => { + const examples = await buildAllExamples(); + persistUpdate((old) => ({ ...old, examples })); + }, }; }