diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b1b22c188..55277b16f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -21,7 +21,7 @@ SveltePlot is a visualization framework based on the [layered grammar of graphic - **Svelte 5.x**: Component framework with modern reactivity - **TypeScript**: Type safety and developer experience - **D3.js ecosystem**: Data manipulation and mathematical utilities - - `d3-array`, `d3-scale`, `d3-shape`, `d3-color`, `d3-interpolate`, etc. + - `d3-array`, `d3-scale`, `d3-shape`, `d3-color`, `d3-interpolate`, etc. - **Vite**: Build tool and development server - **Vitest**: Unit testing framework - **SvelteKit**: Application framework for routing and SSR (for docs) @@ -86,10 +86,10 @@ SveltePlot follows a layered approach where visualizations are built by combinin ```svelte - - - - + + + + ``` @@ -116,24 +116,24 @@ All marks follow consistent patterns: ```svelte ``` @@ -153,12 +153,12 @@ import { scaleLinear, scaleOrdinal } from 'd3-scale'; // Prefer functional programming patterns const processData = (data: DataRow[]) => - data - .filter((d) => d.value != null) - .map((d) => ({ - ...d, - computed: transform(d.value) - })); + data + .filter((d) => d.value != null) + .map((d) => ({ + ...d, + computed: transform(d.value) + })); ``` ### Testing Patterns @@ -169,19 +169,19 @@ import { render } from '@testing-library/svelte'; import Component from './Component.svelte'; test('renders correctly with data', () => { - const { container } = render(Component, { - props: { data: mockData } - }); - expect( - container.querySelector('svg') - ).toBeInTheDocument(); + const { container } = render(Component, { + props: { data: mockData } + }); + expect( + container.querySelector('svg') + ).toBeInTheDocument(); }); // Unit tests for utilities import { transform } from './utility.js'; test('transforms data correctly', () => { - expect(transform(input)).toEqual(expectedOutput); + expect(transform(input)).toEqual(expectedOutput); }); ``` @@ -252,14 +252,14 @@ pnpm run build # Build library ```typescript // Mock data generators const generateTimeSeriesData = (count: number) => - Array.from({ length: count }, (_, i) => ({ - date: new Date(2023, 0, i + 1), - value: Math.random() * 100 - })); + Array.from({ length: count }, (_, i) => ({ + date: new Date(2023, 0, i + 1), + value: Math.random() * 100 + })); // Common test helpers const renderPlot = (marks: any[]) => - render(Plot, { props: { children: marks } }); + render(Plot, { props: { children: marks } }); ``` ### Coverage Expectations diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 44f1445cc..e77c195e3 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -47,9 +47,9 @@ The tilde prefix ("~") is used to avoid conflicts with existing directories in t Before these workflows can function properly, you need to: 1. **Enable GitHub Pages** for your repository: - - Go to repository Settings > Pages - - Set up GitHub Pages to deploy from GitHub Actions - - Make sure the repository has the necessary permissions (pages: write, id-token: write) + - Go to repository Settings > Pages + - Set up GitHub Pages to deploy from GitHub Actions + - Make sure the repository has the necessary permissions (pages: write, id-token: write) 2. No additional credentials are needed as GitHub handles the authentication diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 89ae7439d..5d93c0121 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,8 +33,11 @@ jobs: - name: Install dependencies run: pnpm install + - name: Sync SvelteKit types + run: pnpm exec svelte-kit sync + - name: Run linting (oxlint) - run: pnpm run lint:oxlint + run: pnpm run lint:oxlint 2>&1 - name: Run linting (eslint) run: pnpm run lint:eslint diff --git a/.prettierrc.mjs b/.prettierrc.mjs index f47819e2e..1331695ed 100644 --- a/.prettierrc.mjs +++ b/.prettierrc.mjs @@ -17,11 +17,20 @@ const config = { { files: '*.md', options: { + tabWidth: 2, printWidth: 60, + embeddedLanguageFormatting: 'off', bracketSameLine: true, svelteAllowShorthand: true } }, + { + files: 'src/content/tutorial/**/*.svelte', + options: { + tabWidth: 2, + printWidth: 60 + } + }, { files: '**/routes/examples/**/*.svelte', options: { diff --git a/.svelte-kit/tsconfig.json b/.svelte-kit/tsconfig.json deleted file mode 100644 index 941402a13..000000000 --- a/.svelte-kit/tsconfig.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "compilerOptions": { - "paths": { - "svelteplot": [ - "../packages/svelteplot/src/lib" - ], - "svelteplot/*": [ - "../packages/svelteplot/src/lib/*" - ], - "$shared": [ - "../src/shared" - ], - "$shared/*": [ - "../src/shared/*" - ], - "$lib": [ - "../src/lib" - ], - "$lib/*": [ - "../src/lib/*" - ], - "$app/types": [ - "./types/index.d.ts" - ] - }, - "rootDirs": [ - "..", - "./types" - ], - "verbatimModuleSyntax": true, - "isolatedModules": true, - "lib": [ - "esnext", - "DOM", - "DOM.Iterable" - ], - "moduleResolution": "bundler", - "module": "esnext", - "noEmit": true, - "target": "esnext", - "types": [ - "node" - ] - }, - "include": [ - "ambient.d.ts", - "non-ambient.d.ts", - "./types/**/$types.d.ts", - "../vite.config.js", - "../vite.config.ts", - "../src/**/*.js", - "../src/**/*.ts", - "../src/**/*.svelte", - "../test/**/*.js", - "../test/**/*.ts", - "../test/**/*.svelte", - "../tests/**/*.js", - "../tests/**/*.ts", - "../tests/**/*.svelte" - ], - "exclude": [ - "../node_modules/**", - "../src/service-worker.js", - "../src/service-worker/**/*.js", - "../src/service-worker.ts", - "../src/service-worker/**/*.ts", - "../src/service-worker.d.ts", - "../src/service-worker/**/*.d.ts" - ] -} \ No newline at end of file diff --git a/CANVAS_GUIDE.md b/CANVAS_GUIDE.md index 7cd32bd35..c4741e5dd 100644 --- a/CANVAS_GUIDE.md +++ b/CANVAS_GUIDE.md @@ -150,14 +150,14 @@ Helper component for rendering [MarkName] marks in canvas ```svelte {#snippet children({ scaledData, usedScales })} - {#if canvas} - - {:else} - - {/if} + {#if canvas} + + {:else} + + {/if} {/snippet} ``` @@ -181,8 +181,8 @@ For marks using d3-shape generators (line, area), set the canvas context: const fn = line().curve(curveFactory).context(context); context.beginPath(); fn([ - [x1, y1], - [x2, y2] + [x1, y1], + [x2, y2] ]); context.stroke(); fn.context(null); // reset after loop diff --git a/CLAUDE.md b/CLAUDE.md index dce6147a5..5caa3d53f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ Pure functions that transform the `{data, ...channels}` object. They are compose ```ts const args = $derived( - sort(recordizeXY({ data, x, y, fill, ...rest })) + sort(recordizeXY({ data, x, y, fill, ...rest })) ); ``` diff --git a/config/sidebar.ts b/config/sidebar.ts index 2a744c074..03d7c62cd 100644 --- a/config/sidebar.ts +++ b/config/sidebar.ts @@ -15,6 +15,10 @@ export default { { title: 'Examples', to: '/examples' + }, + { + title: 'Tutorial', + to: '/tutorial' } ] }, diff --git a/package.json b/package.json index c6de1d4e8..7e4e5b30d 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "format": "eslint --fix package.json && prettier --write .", "lint": "pnpm run lint:oxlint && pnpm run lint:eslint && pnpm run lint:types", "lint:eslint": "prettier --check src && eslint src package.json", - "lint:oxlint": "oxlint --type-aware", + "lint:oxlint": "oxlint --type-aware --format=stylish", "lint:types": "svelte-kit sync && tsc --noEmit -p tsconfig.lint-types.json", "preview": "vite preview", "screenshots": "node screenshot-examples.js", @@ -35,11 +35,14 @@ "vr:report": "node scripts/vr-report.js" }, "dependencies": { + "@sveltejs/repl": "workspace:*", "svelteplot": "workspace:*" }, "devDependencies": { "@aitodotai/json-stringify-pretty-compact": "^1.3.0", "@emotion/css": "^11.13.5", + "@rich_harris/svelte-split-pane": "^3.0.0", + "@shikijs/transformers": "^4.0.2", "@shikijs/twoslash": "^3.22.0", "@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-static": "^3.0.10", @@ -81,6 +84,7 @@ "log-update": "^7.1.0", "lru-cache": "^11.3.3", "magic-string": "^0.30.21", + "marked": "^17.0.6", "mdast-util-from-markdown": "^2.0.3", "mdast-util-gfm": "^3.1.0", "oxlint": "^1.59.0", @@ -115,7 +119,8 @@ "vitest": "^4.1.4", "vitest-matchmedia-mock": "^2.0.3", "wx-svelte-grid": "^2.6.1", - "yoctocolors": "^2.1.2" + "yoctocolors": "^2.1.2", + "yootils": "^0.3.1" }, "packageManager": "pnpm@10.33.0" } diff --git a/packages/repl/package.json b/packages/repl/package.json new file mode 100644 index 000000000..a4ccdb42b --- /dev/null +++ b/packages/repl/package.json @@ -0,0 +1,68 @@ +{ + "name": "@sveltejs/repl", + "version": "0.6.0", + "type": "module", + "exports": { + ".": { + "types": "./src/lib/public.d.ts", + "svelte": "./src/lib/index.ts", + "default": "./src/lib/index.ts" + }, + "./bundler": { + "default": "./src/lib/Bundler.svelte.ts" + }, + "./editor": { + "svelte": "./src/lib/Editor/Editor.svelte", + "default": "./src/lib/Editor/Editor.svelte" + }, + "./viewer": { + "svelte": "./src/lib/Output/Viewer.svelte", + "default": "./src/lib/Output/Viewer.svelte" + }, + "./workspace": { + "svelte": "./src/lib/Workspace.svelte.ts", + "default": "./src/lib/Workspace.svelte.ts" + }, + "./console": { + "svelte": "./src/lib/Output/console/index.ts", + "default": "./src/lib/Output/console/index.ts" + } + }, + "peerDependencies": { + "svelte": "^5.0.0" + }, + "dependencies": { + "@codemirror/autocomplete": "^6.20.0", + "@codemirror/commands": "^6.10.2", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/lang-html": "^6.4.11", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/language": "^6.12.1", + "@codemirror/lint": "^6.9.4", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.15", + "@jridgewell/sourcemap-codec": "^1.5.5", + "@lezer/common": "^1.5.1", + "@lezer/highlight": "^1.2.3", + "@replit/codemirror-lang-svelte": "^6.0.0", + "@replit/codemirror-vim": "^6.3.0", + "@rich_harris/svelte-split-pane": "^3.0.0", + "@rollup/browser": "^4.59.0", + "@sveltejs/acorn-typescript": "^1.0.9", + "@sveltejs/svelte-json-tree": "^2.2.1", + "acorn": "^8.16.0", + "codemirror": "^6.0.2", + "d3-dsv": "^3.0.1", + "esm-env": "^1.2.2", + "esrap": "^2.2.5", + "locate-character": "^3.0.0", + "magic-string": "^0.30.21", + "marked": "^17.0.3", + "resolve.exports": "^2.0.3", + "tailwindcss": "^4.2.1", + "tarparser": "^0.0.5", + "zimmerframe": "^1.1.4" + } +} diff --git a/packages/repl/src/app.d.ts b/packages/repl/src/app.d.ts new file mode 100644 index 000000000..c7c0ed1d2 --- /dev/null +++ b/packages/repl/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/packages/repl/src/app.html b/packages/repl/src/app.html new file mode 100644 index 000000000..b2254f390 --- /dev/null +++ b/packages/repl/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/repl/src/lib/Bundler.svelte.ts b/packages/repl/src/lib/Bundler.svelte.ts new file mode 100644 index 000000000..2f75509e5 --- /dev/null +++ b/packages/repl/src/lib/Bundler.svelte.ts @@ -0,0 +1,77 @@ +import type { BundleResult } from './public'; +import type { BundleOptions } from './workers/workers'; +import type { File } from './Workspace.svelte'; + +let uid = 1; + +export default class Bundler { + #worker: Worker; + + result = $state.raw(null); + + constructor({ + svelte_version, + onstatus, + onversion, + onerror + }: { + svelte_version: string; + onstatus: (val: string | null) => void; + onversion?: (version: string, supports_async: boolean) => void; + onerror?: (message: string) => void; + }) { + this.#worker = new Worker(new URL('./workers/bundler/index', import.meta.url), { + type: 'module' + }); + + this.#worker.onmessage = (event) => { + if (event.data.type === 'status') { + onstatus(event.data.message); + return; + } + + if (event.data.type === 'version') { + onversion?.(event.data.version, event.data.supports_async); + return; + } + + if (event.data.type === 'error') { + onerror?.(event.data.message); + return; + } + + onstatus(null); + this.result = event.data; + }; + + this.#worker.postMessage({ type: 'init', svelte_version }); + } + + bundle(files: File[], options: BundleOptions) { + this.#worker.postMessage({ + uid, + type: 'bundle', + files, + options + }); + + uid += 1; + + return new Promise((resolve) => { + const destroy = $effect.root(() => { + let first = true; + $effect.pre(() => { + this.result; + if (first) { + first = false; + } else { + destroy(); + // This isn't necessarily the result of this bundle call, as it could be + // superseeded by another call to `bundle` before the result is set. + resolve(); + } + }); + }); + }); + } +} diff --git a/packages/repl/src/lib/Compiler.ts b/packages/repl/src/lib/Compiler.ts new file mode 100644 index 000000000..5eb2b3c53 --- /dev/null +++ b/packages/repl/src/lib/Compiler.ts @@ -0,0 +1,62 @@ +import { BROWSER } from 'esm-env'; +import type { Compiled, File } from './Workspace.svelte'; + +const callbacks = new Map void>>(); + +let worker: Worker; + +let uid = 1; + +if (BROWSER) { + worker = new Worker(new URL('./workers/compiler/index', import.meta.url), { + type: 'module' + }); + + worker.addEventListener('message', (event) => { + const { filename, id, payload } = event.data; + const file_callbacks = callbacks.get(filename); + + if (file_callbacks) { + const callback = file_callbacks.get(id); + if (callback) { + callback(payload); + file_callbacks.delete(id); + + for (const [other_id, callback] of file_callbacks) { + if (id > other_id) { + callback(payload); + file_callbacks.delete(other_id); + } + } + + if (file_callbacks.size === 0) { + callbacks.delete(filename); + } + } + } + }); +} + +export function compile_file( + file: File, + version: string, + options: { generate: 'client' | 'server'; dev: boolean } +): Promise { + // @ts-ignore + if (!BROWSER) return; + + let id = uid++; + const filename = file.name; + + if (!callbacks.has(filename)) { + callbacks.set(filename, new Map()); + } + + const file_callbacks = callbacks.get(filename)!; + + worker.postMessage({ id, file, version, options }); + + return new Promise((fulfil) => { + file_callbacks.set(id, fulfil); + }); +} diff --git a/packages/repl/src/lib/Editor/Editor.svelte b/packages/repl/src/lib/Editor/Editor.svelte new file mode 100644 index 000000000..298e253d4 --- /dev/null +++ b/packages/repl/src/lib/Editor/Editor.svelte @@ -0,0 +1,122 @@ + + + { + if (!container.contains(e.target as HTMLElement)) { + preserve_editor_focus = false; + } + }} + onmessage={(e) => { + if (preserve_editor_focus && e.data.type === 'iframe_took_focus') { + editor_view.focus(); + } + }} /> + + +
{ + workspace.enable_tab_indent(); + }} + onkeydown={(e) => { + if (e.key !== 'Tab') { + workspace.enable_tab_indent(); + } + }} + onfocusin={(e) => { + clearTimeout(remove_focus_timeout); + preserve_editor_focus = true; + }} + onfocusout={() => { + workspace.disable_tab_indent(); + + // Heuristic: user did refocus themmselves if iframe_took_focus + // doesn't happen in the next few miliseconds. Needed + // because else navigations inside the iframe refocus the editor. + remove_focus_timeout = setTimeout(() => { + preserve_editor_focus = false; + }, 200); + }}> + {#if !BROWSER && workspace.current} +
+
+ {#each workspace.current.contents.split('\n') as _, i} +
{i + 1}
+ {/each} +
+
+ {#each workspace.current.contents.split('\n') as line} +
{line || ' '}
+ {/each} +
+
+ {/if} +
+ + diff --git a/packages/repl/src/lib/Editor/codemirror.css b/packages/repl/src/lib/Editor/codemirror.css new file mode 100644 index 000000000..984b3c652 --- /dev/null +++ b/packages/repl/src/lib/Editor/codemirror.css @@ -0,0 +1,317 @@ +.codemirror-wrapper { + height: 100%; +} + +.cm-editor { + color: var(--shiki-color-text); + background-color: transparent; + height: 100%; + + &.cm-focused { + outline: none; + + .cm-cursor { + border-left-color: var(--sk-fg-3); + } + + > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, + .cm-selectionBackground, + .cm-content ::selection { + background-color: var(--sk-bg-selection); + } + + .cm-matchingBracket, + .cm-nonmatchingBracket { + background-color: #bad0f847; + } + } + + .cm-scroller { + font: var(--sk-font-mono); + } + + .cm-gutters { + background: var(--sk-bg-3); + border-right: 1px solid var(--sk-border); + padding: 0; + } + + .cm-activeLine { + background: inherit; + } + + .cm-foldGutter { + width: 1.4rem; + } + + .cm-activeLineGutter { + /* this must be translucent, or it will obscure the selection */ + background: hsl(0, 0%, 0%, 0.04); + + :root.dark & { + background: hsl(0, 0%, 100%, 0.04); + } + } + + .cm-gutterElement { + position: relative; + + &:where(:has([title='Fold line']), :has([title='Unfold line'])) { + &::after { + content: ''; + position: absolute; + inset: 0; + background: currentColor; + mask: url(icons/chevron.svg) no-repeat 50% 50%; + mask-size: 0.7rem; + transition: transform 0.2s; + cursor: pointer; + rotate: 180deg; + } + } + + &:has([title='Unfold line'])::after { + transform: rotate(90deg); + } + + span { + color: transparent; + } + } + + .cm-lineNumbers { + .cm-gutterElement:not(:last-child) { + display: flex; + justify-content: end; + align-items: end; + } + } + + .cm-foldPlaceholder { + background-color: transparent; + border: none; + color: #ddd; + } + + .cm-lintRange { + background-position: left bottom; + background-repeat: repeat-x; + padding-bottom: 4px; + + &.cm-lintRange-error { + /* TODO */ + } + + &.cm-lintRange-warning { + /* TODO */ + } + } + + .cm-content { + padding: 0.4rem 0; + } + + .cm-line { + padding: 0 1rem; + } + + .cm-selectionBackground { + border-radius: 2px; + background-color: var(--sk-bg-unfocused-selection); + } + + .cm-selectionMatch { + background: var(--selection-color); + color: var(--sk-fg-2); + } + + .cm-tooltip.cm-tooltip-autocomplete { + color: var(--sk-fg-3) !important; + perspective: 1px; + + & > ul > li[aria-selected] { + background-color: var(--sk-bg-4); + color: var(--sk-fg-2) !important; + } + + & > ul { + font: var(--sk-font-mono); + } + } + + .cm-panels { + font: var(--sk-font-ui-small); + background: var(--sk-bg-2); + color: inherit; + + &.cm-panels-top, + &.cm-panels-bottom { + border-top: 1px solid var(--sk-border); + border-bottom: 1px solid var(--sk-border); + } + + .cm-panel { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.5rem 4.2rem 0.5rem 0.5rem; + + .cm-button, + button[aria-label='close'] { + margin: 0; + font: inherit; + background: inherit; + height: 3.2rem; + padding: 0 0.8rem; + border-style: solid; + border-width: var(--sk-raised-width); + border-color: var(--sk-raised-color); + border-radius: var(--sk-border-radius); + + &:hover { + border-color: var(--sk-raised-hover-color); + } + + &:active { + border-color: var(--sk-raised-active-color); + border-width: var(--sk-raised-active-width); + } + } + + button[aria-label='close'] { + aspect-ratio: 1; + top: 0.5rem; + right: 0.5rem; + + &::after { + content: ''; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + background: currentColor; + mask: url(icons/close.svg) 50% 50% no-repeat; + mask-size: 1.8rem; + } + } + + .cm-textfield { + font: inherit; + background: inherit; + color: inherit; + border: 1px solid var(--sk-border); + border-radius: var(--sk-border-radius); + margin: 0; + } + + .cm-search button:focus-visible, + .cm-search input:focus-visible { + border: 2px solid var(--flash); + } + + .cm-search input[type='checkbox']:focus-visible { + outline: 2px solid var(--flash); + } + + label { + font: inherit; + display: inline-flex; + gap: 0.5rem; + align-items: center; + margin: 0 0 0 0.5rem; + } + } + } + + .cm-searchMatch.cm-searchMatch-selected { + background-color: #6199ff2f; + } + + .cm-tooltip { + --warning: hsl(40 100% 70%); + --error: hsl(0 100% 90%); + border: none; + background: var(--sk-bg-3); + font: var(--sk-font-ui-small); + max-width: calc(100vw - 10em); + position: relative; + padding: 1rem; + filter: var(--sk-shadow); + z-index: 9999; + + :root.dark { + --warning: hsl(40 100% 50%); + --error: hsl(0 100% 70%); + } + + &:has(.cm-diagnostic) { + background: transparent; + } + + &:has(.cm-diagnostic-warning) { + --bg: var(--warning); + --fg: #222; + } + + &:has(.cm-diagnostic-error) { + --bg: var(--error); + --fg: #222; + } + + .cm-tooltip-section { + position: relative; + /* left: -1rem; */ + padding: 1rem; + background: var(--bg); + border-radius: 2px; + max-width: 64em; + + .cm-diagnostic { + padding: 0; + margin: 0; + position: relative; + border: none; + border-radius: var(--sk-border-radius); + + &:not(:last-child) { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + margin-bottom: 1rem; + padding-bottom: 1rem; + } + + :not(code) { + font: var(--sk-font-ui-small); + } + + .cm-diagnosticText { + position: relative; + display: flex; + color: var(--fg); + z-index: 2; + + code { + color: inherit; + background-color: rgba(0, 0, 0, 0.05); + font-size: 1em; + margin: 0; /* TODO this counteracts base styles that should probably be deleted? */ + padding: 0.2rem 0.4rem; + } + + a { + font: var(--sk-font-mono); + color: inherit; + font-size: 1em; + /* opacity: 0.7; */ + text-decoration: underline; + } + } + } + } + } + + .highlight { + background: var(--sk-bg-highlight); + padding: 4px 0; + } +} diff --git a/packages/repl/src/lib/Editor/icons/chevron.svg b/packages/repl/src/lib/Editor/icons/chevron.svg new file mode 100644 index 000000000..b19db42d6 --- /dev/null +++ b/packages/repl/src/lib/Editor/icons/chevron.svg @@ -0,0 +1 @@ + diff --git a/packages/repl/src/lib/Editor/icons/close.svg b/packages/repl/src/lib/Editor/icons/close.svg new file mode 100644 index 000000000..d64b59767 --- /dev/null +++ b/packages/repl/src/lib/Editor/icons/close.svg @@ -0,0 +1 @@ + diff --git a/packages/repl/src/lib/Input/ComponentSelector.svelte b/packages/repl/src/lib/Input/ComponentSelector.svelte new file mode 100644 index 000000000..e6f423e00 --- /dev/null +++ b/packages/repl/src/lib/Input/ComponentSelector.svelte @@ -0,0 +1,428 @@ + + +
+ +
+ {#each workspace.files as File[] as file, index (file.name)} +
{ + workspace.select(file.name); + input_value = file.name; + }} + onkeyup={(e) => e.key === ' ' && workspace.select(file.name)} + draggable="true" + ondragstart={() => (dragging = file)} + ondragover={(e) => (e.preventDefault(), (dragover = file))} + ondragleave={(e) => (e.preventDefault(), (dragover = null))} + ondrop={() => { + if (dragging && dragover) { + workspace.move(dragging, dragover); + } + + dragging = dragover = null; + }}> + + + + {(file === workspace.current && file.name !== 'App.svelte' + ? input_value + : file.name) + (workspace.modified[file.name] ? '*' : '') || ' '} + + + {#if file === workspace.current && file.name !== 'App.svelte'} + + { + const input = event.currentTarget; + setTimeout(() => { + input.select(); + }); + }} + onblur={() => close_edit(file)} + onkeydown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + e.currentTarget.blur(); + } + + if (e.key === 'Escape') { + input_value = file.name; + e.currentTarget.blur(); + } + }} /> + + { + // TODO make this a real button, get rid of the keyup listener + remove_file(file); + e.stopPropagation(); + }} + onkeyup={(e) => e.key === ' ' && remove_file(file)}> + + + + + + {/if} +
+ {/each} +
+ + + +
+ + + + + + + + + + + + + {#if download} + + {/if} + + + +
+
+ + diff --git a/packages/repl/src/lib/Input/RunesInfo.svelte b/packages/repl/src/lib/Input/RunesInfo.svelte new file mode 100644 index 000000000..475caafd0 --- /dev/null +++ b/packages/repl/src/lib/Input/RunesInfo.svelte @@ -0,0 +1,125 @@ + + + +
+ + + runes +
+ + {#snippet dropdown()} + + {/snippet} +
+ + diff --git a/packages/repl/src/lib/Message.svelte b/packages/repl/src/lib/Message.svelte new file mode 100644 index 000000000..2187faec6 --- /dev/null +++ b/packages/repl/src/lib/Message.svelte @@ -0,0 +1,88 @@ + + +
+ {#if details} + {message(details)} + {:else} + {@render children?.()} + {/if} +
+ + diff --git a/packages/repl/src/lib/Output/AstNode.svelte b/packages/repl/src/lib/Output/AstNode.svelte new file mode 100644 index 000000000..45de1650a --- /dev/null +++ b/packages/repl/src/lib/Output/AstNode.svelte @@ -0,0 +1,189 @@ + + +
  • + {#if is_primitive || (is_array && value.length === 0)} + + {#if key_text} + {key_text} + {/if} + + {#if value == undefined} + {String(value)} + {:else} + + {typeof value === 'bigint' ? `${value}n` : JSON.stringify(value)} + + {/if} + + {:else} + +
    (e.stopPropagation(), onhover(value))} + onfocusout={() => onhover(null)} + onmouseover={(e) => (e.stopPropagation(), onhover(value))} + onmouseleave={() => onhover(null)} + ontoggle={(e) => { + // toggle events can fire even when the AST output tab is hidden + if (!active) return; + + if (e.currentTarget.open && value && typeof value.start === 'number') { + workspace.highlight_range(value, true); + } + }}> + + {#if key} + {key}: + {/if} + + {#if is_array} + [{#if !open} + ...] + ({value.length}) + {/if} + {:else} + {#if value.type} + {value.type} + {/if} + {'{'}{#if !open}...}{/if} + {/if} + + + +
      { + if (value && typeof value.start === 'number') { + workspace.highlight_range(value, true); + e.stopPropagation(); + } + }}> + {#each Object.entries(value) as [k, v]} + + {/each} +
    + + {is_array ? ']' : '}'} +
    + {/if} +
  • + + diff --git a/packages/repl/src/lib/Output/AstView.svelte b/packages/repl/src/lib/Output/AstView.svelte new file mode 100644 index 000000000..e4fd8b802 --- /dev/null +++ b/packages/repl/src/lib/Output/AstView.svelte @@ -0,0 +1,148 @@ + + +
    +
    +		
    +			{#if typeof ast === 'object'}
    +                
      + { + if ( + node === null || + (node.type !== undefined && + node.start !== undefined && + node.end !== undefined) + ) { + cursor = node && node.start + 1; + workspace.highlight_range(node); + } + }} /> +
    + {:else} +

    No AST available

    + {/if} +
    +
    + + The AST is not public API and may change at any point in time + + +
    + + diff --git a/packages/repl/src/lib/Output/CompilerOptions.svelte b/packages/repl/src/lib/Output/CompilerOptions.svelte new file mode 100644 index 000000000..9095a5fa5 --- /dev/null +++ b/packages/repl/src/lib/Output/CompilerOptions.svelte @@ -0,0 +1,147 @@ + + +
    + result = svelte.compile(source, { +
    + generate: + + {#each ['client', 'server'] as const as generate} + { + workspace.update_compiler_options({ generate }); + }} /> + + {/each}, +
    + + {#if is_fragments_available} +
    + fragments: + + {#each ['html', 'tree'] as const as fragments} + { + workspace.update_compiler_options({ fragments }); + }} /> + + {/each}, +
    + {/if} + + + + }); +
    + + diff --git a/packages/repl/src/lib/Output/ErrorOverlay.svelte b/packages/repl/src/lib/Output/ErrorOverlay.svelte new file mode 100644 index 000000000..62ef00fed --- /dev/null +++ b/packages/repl/src/lib/Output/ErrorOverlay.svelte @@ -0,0 +1,48 @@ + + +
    +
    +

    Error compiling {error.filename ?? 'component'}

    +
    {error.message}
    + + {#if error.start} + line {error.start.line} column {error.start.column} + {/if} +
    +
    + + diff --git a/packages/repl/src/lib/Output/Output.svelte b/packages/repl/src/lib/Output/Output.svelte new file mode 100644 index 000000000..e5d951418 --- /dev/null +++ b/packages/repl/src/lib/Output/Output.svelte @@ -0,0 +1,304 @@ + + +{#if embedded !== 'output-only'} +
    + {#if workspace.current.name.endsWith('.md')} + + {:else} + + + + + {/if} +
    +{/if} + + +
    + {} : undefined} + theme={previewTheme} /> +
    + + +
    + {#if embedded} + + {:else} + + {#snippet main()} + + {/snippet} + + {#snippet body()} + + {/snippet} + + {/if} +
    + + +
    + +
    + + +{#if current?.result} +
    + +
    +{/if} + + +
    + +
    + + diff --git a/packages/repl/src/lib/Output/PaneWithPanel.svelte b/packages/repl/src/lib/Output/PaneWithPanel.svelte new file mode 100644 index 000000000..91b3c8fee --- /dev/null +++ b/packages/repl/src/lib/Output/PaneWithPanel.svelte @@ -0,0 +1,143 @@ + + +
    + + {#snippet a()} +
    + {@render main?.()} +
    + {/snippet} + + {#snippet b()} +
    +
    + + + {@render header?.()} +
    + +
    + {@render body?.()} +
    +
    + {/snippet} +
    +
    + + diff --git a/packages/repl/src/lib/Output/ReplProxy.ts b/packages/repl/src/lib/Output/ReplProxy.ts new file mode 100644 index 000000000..4b4620f2b --- /dev/null +++ b/packages/repl/src/lib/Output/ReplProxy.ts @@ -0,0 +1,97 @@ +import type { Handlers } from './proxy'; + +let uid = 1; + +export default class ReplProxy { + iframe: HTMLIFrameElement; + handlers: Handlers; + pending_cmds: Map void; reject: (value: any) => void }> = + new Map(); + + handle_event = (event: MessageEvent) => { + if (event.source !== this.iframe.contentWindow) return; + + const { action, args } = event.data; + + switch (action) { + case 'cmd_error': + case 'cmd_ok': + return this.handle_command_message(event.data); + case 'fetch_progress': + return this.handlers.on_fetch_progress(args.remaining); + case 'error': + return this.handlers.on_error(event.data); + case 'unhandledrejection': + return this.handlers.on_unhandled_rejection(event.data); + case 'iframe_reload': + return this.handlers.on_iframe_reload(event.data); + case 'console': + if (event.data.command === 'info' && event.data.args[0]?.type === '__error') { + const data = event.data.args[0]; + const e = new Error(data.message); + e.name = data.name; + e.stack = data.stack; + event.data.args[0] = e; + } + + return this.handlers.on_console(event.data); + } + }; + + constructor(iframe: HTMLIFrameElement, handlers: Handlers) { + this.iframe = iframe; + this.handlers = handlers; + + window.addEventListener('message', this.handle_event, false); + } + + destroy() { + window.removeEventListener('message', this.handle_event); + } + + iframe_command(action: string, args: any) { + return new Promise((resolve, reject) => { + const cmd_id = uid++; + + this.pending_cmds.set(cmd_id, { resolve, reject }); + + this.iframe.contentWindow?.postMessage({ action, cmd_id, args }, '*'); + }); + } + + handle_command_message(cmd_data: { + action: string; + cmd_id: number; + message: string; + stack: any; + args: any; + }) { + let action = cmd_data.action; + let id = cmd_data.cmd_id; + let handler = this.pending_cmds.get(id); + + if (handler) { + this.pending_cmds.delete(id); + if (action === 'cmd_error') { + let { message, stack } = cmd_data; + let e = new Error(message); + e.stack = stack; + handler.reject(e); + } + + if (action === 'cmd_ok') { + handler.resolve(cmd_data.args); + } + } else { + console.error('command not found', id, cmd_data, [...this.pending_cmds.keys()]); + } + } + + eval(script: string, style?: string) { + return this.iframe_command('eval', { script, style }); + } + + handle_links() { + return this.iframe_command('catch_clicks', {}); + } +} diff --git a/packages/repl/src/lib/Output/Viewer.svelte b/packages/repl/src/lib/Output/Viewer.svelte new file mode 100644 index 000000000..b99e7de39 --- /dev/null +++ b/packages/repl/src/lib/Output/Viewer.svelte @@ -0,0 +1,449 @@ + + +{#snippet main()} + + +
    + {#if bundle?.error} + + {:else if error} + + {:else if status || !bundle} + {status || 'loading Svelte compiler...'} + {/if} +
    +{/snippet} + +
    + {#if !onLog} + + {#snippet header()} + + {/snippet} + + {#snippet body()} + + {/snippet} + + {:else} + {@render main()} + {/if} +
    + + diff --git a/packages/repl/src/lib/Output/console/Console.svelte b/packages/repl/src/lib/Output/console/Console.svelte new file mode 100644 index 000000000..3d92e3ece --- /dev/null +++ b/packages/repl/src/lib/Output/console/Console.svelte @@ -0,0 +1,58 @@ + + +
    + {#each logs as log} + + {/each} +
    + + diff --git a/packages/repl/src/lib/Output/console/ConsoleLine.svelte b/packages/repl/src/lib/Output/console/ConsoleLine.svelte new file mode 100644 index 000000000..1cde7eeb4 --- /dev/null +++ b/packages/repl/src/lib/Output/console/ConsoleLine.svelte @@ -0,0 +1,326 @@ + + +{#if log.command === 'table'} + +{/if} + +
    + +
    + {#if log.count && log.count > 1} + {log.count} + {/if} + + {#if log.stack || log.command === 'group'} + {'\u25B6'} + {/if} + + {#if log.command === 'clear'} + Console was cleared + {:else if log.command === 'unclonable'} + Message could not be cloned. Open devtools to see it + {:else if log.command === 'table'} + + {:else} + + {#each format_args(log.args) as part} + + {#if !part.formatted} + {' '} + {/if}{#if part.type === 'value'} + {#if part.value instanceof Error} +
    {part.value.name +
    +                                    '\n' +
    +                                    part.value.stack.replace(/^\n+/, '')}
    + {:else} + + {/if} + {:else} + + e.stopPropagation()}> + {@html link(escape_html(part.value))} + + {/if} + {/each} +
    + {/if} +
    + + {#if log.stack && !log.collapsed} +
    + {#each log.stack as line} + {line.label} + {line.location} + {/each} +
    + {/if} + + {#each new Array(depth) as _, idx} +
    + {/each} +
    + +{#if log.command === 'group' && !log.collapsed} + {#each log.logs ?? [] as childLog} + + {/each} +{/if} + + diff --git a/packages/repl/src/lib/Output/console/ConsoleTable.svelte b/packages/repl/src/lib/Output/console/ConsoleTable.svelte new file mode 100644 index 000000000..646bd1add --- /dev/null +++ b/packages/repl/src/lib/Output/console/ConsoleTable.svelte @@ -0,0 +1,139 @@ + + +
    + + + + + + {#each table.columns as column} + + {/each} + + + + + {#each table.rows as row} + + + + {#each row.values as value} + + {/each} + + {/each} + +
    (index){column}
    + {#if typeof row.key === 'string'} + {row.key} + {:else} + + {/if} + + {#if typeof value === 'string'} + {value} + {:else} + + {/if} +
    +
    + + diff --git a/packages/repl/src/lib/Output/console/Log.svelte.ts b/packages/repl/src/lib/Output/console/Log.svelte.ts new file mode 100644 index 000000000..a4d4c2042 --- /dev/null +++ b/packages/repl/src/lib/Output/console/Log.svelte.ts @@ -0,0 +1,22 @@ +type Command = 'info' | 'warn' | 'error' | 'table' | 'group' | 'clear' | 'unclonable'; + +export class Log { + command: Command; + args?: any[]; + stack?: Array<{ + label?: string; + location?: string; + }>; + data?: any; + columns?: string[]; + + collapsed = $state(false); + expanded = $state(false); + count = $state(1); + logs = $state([]); + + constructor(data: any) { + this.command = data.command; + Object.assign(this, data); + } +} diff --git a/packages/repl/src/lib/Output/console/index.ts b/packages/repl/src/lib/Output/console/index.ts new file mode 100644 index 000000000..463db6060 --- /dev/null +++ b/packages/repl/src/lib/Output/console/index.ts @@ -0,0 +1,2 @@ +export { default as Console } from './Console.svelte'; +export { Log } from './Log.svelte'; diff --git a/packages/repl/src/lib/Output/get-location-from-stack.ts b/packages/repl/src/lib/Output/get-location-from-stack.ts new file mode 100644 index 000000000..be9489fe3 --- /dev/null +++ b/packages/repl/src/lib/Output/get-location-from-stack.ts @@ -0,0 +1,33 @@ +import { decode } from '@jridgewell/sourcemap-codec'; +import type { StartOrEnd } from '../types'; +import type { SourceMap } from '@rollup/browser'; + +export default function getLocationFromStack(stack: string, map: SourceMap) { + if (!stack) return; + const last = stack.split('\n')[1]; + const match = /:(\d+):(\d+)\)$/.exec(last); + + if (!match) return null; + + const line = +match[1]; + const column = +match[2]; + + return trace({ line, column }, map); +} + +function trace(loc: Omit, map: SourceMap) { + const mappings = decode(map.mappings); + const segments = mappings[loc.line - 1]; + + for (let i = 0; i < segments.length; i += 1) { + const segment = segments[i]; + if (segment[0] === loc.column) { + const [, sourceIndex, line, column] = segment; + const source = map.sources[sourceIndex ?? 0].slice(2); + + return { source, line: (line ?? 0) + 1, column }; + } + } + + return null; +} diff --git a/packages/repl/src/lib/Output/proxy.d.ts b/packages/repl/src/lib/Output/proxy.d.ts new file mode 100644 index 000000000..b69a7b5ad --- /dev/null +++ b/packages/repl/src/lib/Output/proxy.d.ts @@ -0,0 +1,4 @@ +export type Handlers = Record< + 'on_fetch_progress' | 'on_error' | 'on_unhandled_rejection' | 'on_iframe_reload' | 'on_console', + (data: any) => void +>; diff --git a/packages/repl/src/lib/Output/srcdoc/index.html b/packages/repl/src/lib/Output/srcdoc/index.html new file mode 100644 index 000000000..39af52bc0 --- /dev/null +++ b/packages/repl/src/lib/Output/srcdoc/index.html @@ -0,0 +1,288 @@ + + + + + + + + + diff --git a/packages/repl/src/lib/Output/srcdoc/styles.css b/packages/repl/src/lib/Output/srcdoc/styles.css new file mode 100644 index 000000000..07d14f255 --- /dev/null +++ b/packages/repl/src/lib/Output/srcdoc/styles.css @@ -0,0 +1,63 @@ +body { + --bg-1: hsl(0, 0%, 100%); + --bg-2: hsl(206, 20%, 90%); + --bg-3: hsl(206, 20%, 80%); + --fg-1: hsl(0, 0%, 13%); + --fg-2: hsl(0, 0%, 20%); + --fg-2: hsl(0, 0%, 30%); + --link: hsl(208, 77%, 47%); + --link-hover: hsl(208, 77%, 55%); + --link-active: hsl(208, 77%, 40%); + --border-radius: 4px; + --font: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, + 'Open Sans', 'Helvetica Neue', sans-serif; + --font-mono: + ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', + monospace; + color-scheme: light; + background: var(--bg-1); + color: var(--fg-1); + font-family: var(--font); + line-height: 1.5; + margin: 1rem; + height: calc(100vh - 2rem); + accent-color: var(--hover) !important; +} + +a { + color: var(--link); +} + +a:hover { + color: var(--link-hover); +} + +a:active { + color: var(--link-active); +} + +code { + background: var(--bg-2); + font-family: var(--font-mono); + font-size: 0.9em; + padding: 0.15rem 0.3rem; + border-radius: var(--border-radius); +} + +ul.todos { + padding: 0; +} + +body.dark { + color-scheme: dark; + --bg-1: hsl(0, 0%, 18%); + --bg-2: hsl(0, 0%, 30%); + --bg-3: hsl(0, 0%, 40%); + --fg-1: hsl(0, 0%, 90%); + --fg-2: hsl(0, 0%, 70%); + --fg-3: hsl(0, 0%, 60%); + --link: hsl(206, 96%, 72%); + --link-hover: hsl(206, 96%, 78%); + --link-active: hsl(206, 96%, 64%); +} diff --git a/packages/repl/src/lib/Repl.svelte b/packages/repl/src/lib/Repl.svelte new file mode 100644 index 000000000..4643752c7 --- /dev/null +++ b/packages/repl/src/lib/Repl.svelte @@ -0,0 +1,359 @@ + + + + +
    +
    + + {#snippet a()} +
    + + + +
    + {/snippet} + + {#snippet b()} +
    + +
    + {/snippet} +
    +
    + + {#if $toggleable} + showOutput, + (v) => { + toggled ||= true; + showOutput = v; + } + } /> + {/if} +
    + + diff --git a/packages/repl/src/lib/Workspace.svelte.ts b/packages/repl/src/lib/Workspace.svelte.ts new file mode 100644 index 000000000..6b6ad02ff --- /dev/null +++ b/packages/repl/src/lib/Workspace.svelte.ts @@ -0,0 +1,771 @@ +import type { CompileError, CompileResult } from 'svelte/compiler'; +import { Compartment, EditorState, StateEffect, StateField } from '@codemirror/state'; +import { compile_file } from './Compiler'; +import { BROWSER } from 'esm-env'; +import { basicSetup, EditorView } from 'codemirror'; +import { javascript } from '@codemirror/lang-javascript'; +import { html } from '@codemirror/lang-html'; +import { svelte } from '@replit/codemirror-lang-svelte'; +import { autocomplete_for_svelte } from '@sveltejs/site-kit/codemirror'; +import { Decoration, keymap, type DecorationSet } from '@codemirror/view'; +import { acceptCompletion } from '@codemirror/autocomplete'; +import { indentWithTab } from '@codemirror/commands'; +import { indentUnit } from '@codemirror/language'; +import { theme } from './theme'; +import { untrack } from 'svelte'; +import type { Diagnostic } from '@codemirror/lint'; + +export interface File { + type: 'file'; + name: string; + basename: string; + contents: string; + text: boolean; +} + +export interface Directory { + type: 'directory'; + name: string; + basename: string; +} + +export type Item = File | Directory; + +export interface Compiled { + error: CompileError | null; + result: CompileResult | null; + migration: { + code: string; + } | null; +} + +function is_file(item: Item): item is File { + return item.type === 'file'; +} + +function is_svelte_file(file: File) { + return /\.svelte(\.|$)/.test(file.name); +} + +function file_type(file: Item) { + return file.name.split('.').pop(); +} + +const set_highlight = StateEffect.define<{ start: number; end: number } | null>(); + +const highlight_field = StateField.define({ + create() { + return Decoration.none; + }, + update(highlights, tr) { + // Apply the effect + for (let effect of tr.effects) { + if (effect.is(set_highlight)) { + if (effect.value) { + const { start, end } = effect.value; + const deco = Decoration.mark({ class: 'highlight' }).range(start, end); + return Decoration.set([deco]); + } else { + // Clear highlight + return Decoration.none; + } + } + } + // Map decorations for document changes + return highlights.map(tr.changes); + }, + provide: (field) => EditorView.decorations.from(field) +}); + +const tab_behaviour = new Compartment(); +const vim_mode = new Compartment(); + +const default_extensions = [ + basicSetup, + EditorState.tabSize.of(2), + tab_behaviour.of(keymap.of([{ key: 'Tab', run: acceptCompletion }])), + indentUnit.of('\t'), + theme, + vim_mode.of([]), + highlight_field +]; + +export interface ExposedCompilerOptions { + generate: 'client' | 'server'; + dev: boolean; + modernAst: boolean; + fragments: 'html' | 'tree' | undefined; + async: boolean; +} + +export class Workspace { + // TODO this stuff should all be readonly + creating = $state.raw<{ parent: string; type: 'file' | 'directory' } | null>(null); + modified = $state>({}); + + #compiler_options = $state.raw({ + generate: 'client', + dev: false, + modernAst: true, + // default to undefined so it's removed if the current version + // doesn't support it + fragments: undefined, + async: true + }); + compiled = $state>({}); + + #svelte_version = $state(''); + #readonly = false; // TODO do we need workspaces for readonly stuff? + #files = $state.raw([]); + #current = $state.raw() as File; + #vim = $state(false); + #aliases = $state.raw(undefined) as undefined | Record; + #tailwind = $state(false); + supports_async = $state(true); + + #handlers = { + hover: new Set<(pos: number | null) => void>(), + select: new Set<(from: number, to: number) => void>() + }; + + #onupdate: (file: File) => void; + #onreset: (items: Item[]) => void | Promise; + + // CodeMirror stuff + states = new Map(); + #view: EditorView | null = null; + + diagnostics = $derived.by(() => { + const diagnostics: Diagnostic[] = []; + + const error = this.current_compiled?.error; + const warnings = this.current_compiled?.result?.warnings ?? []; + + if (error) { + diagnostics.push({ + severity: 'error', + from: error.position![0], + to: error.position![1], + message: error.message, + renderMessage: () => { + let html = error.message + .replace(/&/g, '&') + .replace(/$1`); + + if (error.code) { + html += ` (${error.code})`; + } + + const span = document.createElement('span'); + span.innerHTML = html; + + return span; + } + }); + } + + for (const warning of warnings) { + diagnostics.push({ + severity: 'warning', + from: warning.start!.character, + to: warning.end!.character, + message: warning.message, + renderMessage: () => { + const span = document.createElement('span'); + span.innerHTML = `${warning.message + .replace(/&/g, '&') + .replace(/$1` + )} (${warning.code})`; + + return span; + } + }); + } + + return diagnostics; + }); + + constructor( + files: Item[], + { + svelte_version = 'latest', + initial, + readonly = false, + onupdate, + onreset + }: { + svelte_version?: string; + initial?: string; + readonly?: boolean; + onupdate?: (file: File) => void; + onreset?: (items: Item[]) => void | Promise; + } = {} + ) { + this.#svelte_version = svelte_version; + this.#readonly = readonly; + + this.set(files, initial); + + this.#onupdate = onupdate ?? (() => {}); + this.#onreset = onreset ?? (() => {}); + + this.#reset_diagnostics(); + } + + get files() { + return this.#files; + } + + get compiler_options() { + return this.#compiler_options; + } + + get current() { + return this.#current; + } + + get current_compiled() { + if (this.#current.name in this.compiled) { + return this.compiled[this.#current.name]; + } + + return null; + } + + add(item: Item) { + this.#create_directories(item); + this.#files = this.#files.concat(item); + + if (is_file(item)) { + this.#select(item); + this.#onreset?.(this.#files); + + this.modified[item.name] = true; + } + + return item; + } + + disable_tab_indent() { + this.#view?.dispatch({ + effects: tab_behaviour.reconfigure(keymap.of([{ key: 'Tab', run: acceptCompletion }])) + }); + } + + enable_tab_indent() { + this.#view?.dispatch({ + effects: tab_behaviour.reconfigure( + keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]) + ) + }); + } + + focus() { + setTimeout(() => { + this.#view?.focus(); + }); + } + + highlight_range(node: { start: number; end: number } | null, scroll = false) { + if (!this.#view) return; + + const effects: StateEffect[] = [set_highlight.of(node)]; + + if (scroll && node) { + effects.push(EditorView.scrollIntoView(node.start, { y: 'center' })); + } + + this.#view.dispatch({ + effects + }); + } + + mark_saved() { + this.modified = {}; + } + + async link(view: EditorView) { + if (this.#view) throw new Error('view is already linked'); + this.#view = view; + + untrack(() => { + view.setState(this.#get_state(untrack(() => this.#current))); + this.vim = localStorage.getItem('vim') === 'true'; + }); + } + + move(from: Item, to: Item) { + const from_index = this.#files.indexOf(from); + const to_index = this.#files.indexOf(to); + + this.#files.splice(from_index, 1); + + this.#files = this.#files + .slice(0, to_index) + .concat(from) + .concat(this.#files.slice(to_index)); + } + + onhover(fn: (pos: number | null) => void) { + $effect(() => { + this.#handlers.hover.add(fn); + + return () => { + this.#handlers.hover.delete(fn); + }; + }); + } + + onselect(fn: (from: number, to: number) => void) { + $effect(() => { + this.#handlers.select.add(fn); + + return () => { + this.#handlers.select.delete(fn); + }; + }); + } + + remove(item: Item) { + const index = this.#files.indexOf(item); + + if (index === -1) { + throw new Error('Tried to remove a file that does not exist in the workspace'); + } + + let next = this.#current; + + if (next === item) { + const file = + this.#files.slice(0, index).findLast(is_file) ?? + this.#files.slice(index + 1).find(is_file); + + if (!file) { + throw new Error('Cannot delete the only file'); + } + + next = file; + } + + this.#files = this.#files.filter((f) => { + if (f === item) return false; + if (f.name.startsWith(item.name + '/')) return false; + return true; + }); + + this.#select(next); + + this.#onreset?.(this.#files); + } + + rename(previous: Item, name: string) { + const index = this.files.indexOf(previous); + const was_current = previous === this.#current; + + const state = this.states.get(previous.name); + this.states.delete(previous.name); + + const new_item: Item = { + ...previous, + name, + basename: name.split('/').pop()! + }; + + this.#create_directories(new_item); + + this.#files = this.#files.map((item, i) => { + if (i === index) return new_item; + + if (previous.type === 'directory' && item.name.startsWith(previous.name + '/')) { + return { + ...item, + name: item.name.replace(previous.name, name) + }; + } + + return item; + }); + + // preserve state, unless the language changed (in which case + // it's simpler to just create a new editor state) + if (state && file_type(previous) === file_type(new_item)) { + this.states.set(name, state); + } + + if (was_current) { + this.#select(new_item as File); + } + + if (this.modified[previous.name]) { + delete this.modified[previous.name]; + this.modified[name] = true; + } + + this.#onreset?.(this.#files); + } + + reset( + new_files: Item[], + options: { tailwind: boolean; aliases?: Record }, + selected?: string + ) { + this.states.clear(); + this.#tailwind = options.tailwind; + this.#aliases = options.aliases; + + const bundle = this.set(new_files, selected); + + this.mark_saved(); + + const diagnostics = this.#reset_diagnostics(); + + return Promise.all([bundle, diagnostics]) + .then(() => {}) + .catch(() => {}); + } + + select(name: string) { + const file = this.#files.find((file) => is_file(file) && file.name === name); + + if (!file) { + throw new Error(`File ${name} does not exist in workspace`); + } + + this.#select(file as File); + } + + set(files: Item[], selected = this.#current?.name) { + const first = files.find(is_file); + + if (!first) { + throw new Error('Workspace must have at least one file'); + } + + const matching_file = + selected && files.find((file) => is_file(file) && file.name === selected); + if (matching_file) { + this.#select(matching_file as File); + } else { + this.#select(first); + } + + this.#files = files; + + for (const [name, state] of this.states) { + const file = files.find((file) => file.name === name) as File; + + if (file) { + this.#update_state(file, state); + } else { + this.states.delete(name); + } + } + + return this.#onreset?.(this.files); + } + + unlink(view: EditorView) { + if (this.#view !== view) throw new Error('Wrong editor view'); + this.#view = null; + } + + update_compiler_options(options: Partial) { + this.#compiler_options = { ...this.#compiler_options, ...options }; + this.#reset_diagnostics(); + for (let file of this.#files) { + if (is_file(file)) { + this.#onupdate(file); + } + } + } + + update_file(file: File) { + this.#update_file(file); + + const state = this.states.get(file.name); + if (state) { + this.#update_state(file, state); + } + } + + get aliases() { + return this.#aliases; + } + + set aliases(value) { + this.#aliases = value; + this.#onupdate(this.#current); + } + + get tailwind() { + return this.#tailwind; + } + + set tailwind(value) { + this.#tailwind = value; + this.#onupdate(this.#current); + } + + get svelte_version() { + return this.#svelte_version; + } + + set_svelte_version(value: string, notify = false) { + this.#svelte_version = value; + if (notify) { + this.#update_file(this.#current); + this.#reset_diagnostics(); + } + } + + get vim() { + return this.#vim; + } + + set vim(value) { + this.#toggle_vim(value); + } + + async #toggle_vim(value: boolean) { + this.#vim = value; + + localStorage.setItem('vim', String(value)); + + let vim_extension_index = default_extensions.findIndex( + // @ts-ignore jfc CodeMirror is a struggle + (ext: any) => ext.compartment === vim_mode + ); + + let extension: any = []; + + if (value) { + const { vim } = await import('@replit/codemirror-vim'); + extension = vim(); + } + + default_extensions[vim_extension_index] = vim_mode.of(extension); + + this.#view?.dispatch({ + effects: vim_mode.reconfigure(extension) + }); + + // update all the other states + for (const file of this.#files) { + if (file.type !== 'file') continue; + if (file === this.#current) continue; + + this.states.set(file.name, this.#create_state(file)); + } + } + + #create_directories(item: Item) { + // create intermediate directories as necessary + const parts = item.name.split('/'); + + while (parts.length > 1) { + parts.pop(); + const joined = parts.join('/'); + + if (this.files.find((file) => file.name === joined)) { + return; + } + + this.#files.push({ + type: 'directory', + name: joined, + basename: joined.split('/').pop()! // TODO get rid of this basename nonsense, it's infuriating + }); + } + } + + #get_state(file: File) { + return this.states.get(file.name) ?? this.#create_state(file); + } + + #create_state(file: File) { + const extensions = [ + ...default_extensions, + EditorState.readOnly.of(this.#readonly), + EditorView.editable.of(!this.#readonly), + EditorView.updateListener.of((update) => { + const state = this.#view!.state!; + + if (update.docChanged) { + this.#update_file({ + ...this.#current, + contents: state.doc.toString() + }); + + // preserve undo/redo across files + this.states.set(this.#current.name, state); + } + + if (update.selectionSet) { + if (state.selection.ranges.length === 1) { + for (const handler of this.#handlers.select) { + const { from, to } = state.selection.ranges[0]; + handler(from, to); + } + } + } + }), + EditorView.domEventObservers({ + mousemove: (event, view) => { + const pos = view.posAtCoords({ x: event.clientX, y: event.clientY }); + + if (pos !== null) { + for (const handler of this.#handlers.hover) { + handler(pos); + } + } + }, + mouseleave: (event, view) => { + for (const handler of this.#handlers.hover) { + handler(null); + } + } + }) + ]; + + switch (file_type(file)) { + case 'js': // TODO autocomplete, including runes + case 'json': + extensions.push(javascript()); + break; + + case 'ts': + extensions.push(javascript({ typescript: true })); + break; + + case 'html': + extensions.push(html()); + break; + + case 'svelte': + extensions.push( + svelte(), + ...autocomplete_for_svelte( + () => this.current.name, + () => + this.files + .filter((file) => { + if (file.type !== 'file') return false; + // TODO put autocomplete_filter on the workspace + // return autocomplete_filter(file); + return true; + }) + .map((file) => file.name) + ) + ); + break; + } + + const state = EditorState.create({ + doc: file.contents, + extensions + }); + + this.states.set(file.name, state); + + return state; + } + + #reset_diagnostics() { + if (!BROWSER) return; + + const keys = Object.keys(this.compiled); + const seen: string[] = []; + + let files = this.#files; + + // prioritise selected file + if (this.current) { + const i = this.#files.indexOf(this.current!); + files = [this.current, ...this.#files.slice(0, i), ...this.#files.slice(i + 1)]; + } + + const done = files.map((file) => { + if (file.type !== 'file') return; + if (!is_svelte_file(file)) return; + + seen.push(file.name); + + return compile_file(file, this.#svelte_version, this.compiler_options).then( + (compiled) => { + this.compiled[file.name] = compiled; + } + ); + }); + + for (const key of keys) { + if (!seen.includes(key)) { + delete this.compiled[key]; + } + } + + return Promise.all(done) + .then(() => {}) + .catch(() => {}); + } + + #select(file: File) { + this.#current = file as File; + this.#view?.setState(this.#get_state(this.#current)); + } + + #update_file(file: File) { + if (file.name === this.#current.name) { + this.#current = file; + } + + this.#files = this.#files.map((old) => { + if (old.name === file.name) { + return file; + } + return old; + }); + + this.modified[file.name] = true; + + if (BROWSER && is_svelte_file(file)) { + compile_file(file, this.#svelte_version, this.compiler_options).then((compiled) => { + this.compiled[file.name] = compiled; + }); + } + + this.#onupdate(file); + } + + #update_state(file: File, state: EditorState) { + const existing = state.doc.toString(); + + if (file.contents !== existing) { + const current_cursor_position = Math.min( + this.#view?.state.selection.ranges[0].from!, + file.contents.length + ); + + const transaction = state.update({ + changes: { + from: 0, + to: existing.length, + insert: file.contents + }, + selection: { + anchor: current_cursor_position, + head: current_cursor_position + } + }); + + this.states.set(file.name, transaction.state); + + if (file === this.#current) { + this.#view?.setState(transaction.state); + } + } + } +} diff --git a/packages/repl/src/lib/context.ts b/packages/repl/src/lib/context.ts new file mode 100644 index 000000000..d37eb32c9 --- /dev/null +++ b/packages/repl/src/lib/context.ts @@ -0,0 +1,12 @@ +import { getContext, setContext } from 'svelte'; +import type { ReplContext } from './types'; + +const key = Symbol('repl'); + +export function get_repl_context(): ReplContext { + return getContext(key); +} + +export function set_repl_context(value: ReplContext) { + setContext(key, value); +} diff --git a/packages/repl/src/lib/index.ts b/packages/repl/src/lib/index.ts new file mode 100644 index 000000000..e46b51da0 --- /dev/null +++ b/packages/repl/src/lib/index.ts @@ -0,0 +1 @@ +export { default as Repl } from './Repl.svelte'; diff --git a/packages/repl/src/lib/public.d.ts b/packages/repl/src/lib/public.d.ts new file mode 100644 index 000000000..55a3ca0c7 --- /dev/null +++ b/packages/repl/src/lib/public.d.ts @@ -0,0 +1,13 @@ +import type { OutputChunk, RollupError } from '@rollup/browser'; +import type { CompileError } from 'svelte/compiler'; + +export interface BundleResult { + uid: number; + error: (RollupError & CompileError) | null; + client: OutputChunk | null; + server: OutputChunk | null; + css: string | null; + imports: string[]; +} + +export * from './index'; diff --git a/packages/repl/src/lib/site-kit/codemirror/autocompletionDataProvider.js b/packages/repl/src/lib/site-kit/codemirror/autocompletionDataProvider.js new file mode 100644 index 000000000..c5c87f63b --- /dev/null +++ b/packages/repl/src/lib/site-kit/codemirror/autocompletionDataProvider.js @@ -0,0 +1,609 @@ +/** + * this file is based on [dataProvider.ts from sveltejs/language-tools](https://github.com/sveltejs/language-tools/blob/master/packages/language-server/src/plugins/html/dataProvider.ts) + */ + +export const globalEvents = [ + { name: 'onabort' }, + { name: 'onanimationcancel' }, + { name: 'onanimationend' }, + { name: 'onanimationiteration' }, + { name: 'onanimationstart' }, + { name: 'onauxclick' }, + { name: 'onbeforeinput' }, + { name: 'onblur' }, + { name: 'oncancel' }, + { name: 'oncanplay' }, + { name: 'oncanplaythrough' }, + { name: 'onchange' }, + { name: 'onclick' }, + { name: 'onclose' }, + { name: 'oncontextmenu' }, + { name: 'oncopy' }, + { name: 'oncuechange' }, + { name: 'oncut' }, + { name: 'ondblclick' }, + { name: 'ondrag' }, + { name: 'ondragend' }, + { name: 'ondragenter' }, + { name: 'ondragleave' }, + { name: 'ondragover' }, + { name: 'ondragstart' }, + { name: 'ondrop' }, + { name: 'ondurationchange' }, + { name: 'onemptied' }, + { name: 'onended' }, + { name: 'onerror' }, + { name: 'onfocus' }, + { name: 'onformdata' }, + { name: 'ongotpointercapture' }, + { name: 'oninput' }, + { name: 'oninvalid' }, + { name: 'onkeydown' }, + { name: 'onkeypress' }, + { name: 'onkeyup' }, + { name: 'onload' }, + { name: 'onloadeddata' }, + { name: 'onloadedmetadata' }, + { name: 'onloadstart' }, + { name: 'onlostpointercapture' }, + { name: 'onmousedown' }, + { name: 'onmouseenter' }, + { name: 'onmouseleave' }, + { name: 'onmousemove' }, + { name: 'onmouseout' }, + { name: 'onmouseover' }, + { name: 'onmouseup' }, + { name: 'onpaste' }, + { name: 'onpause' }, + { name: 'onplay' }, + { name: 'onplaying' }, + { name: 'onpointercancel' }, + { name: 'onpointerdown' }, + { name: 'onpointerenter' }, + { name: 'onpointerleave' }, + { name: 'onpointermove' }, + { name: 'onpointerout' }, + { name: 'onpointerover' }, + { name: 'onpointerup' }, + { name: 'onprogress' }, + { name: 'onratechange' }, + { name: 'onreset' }, + { name: 'onresize' }, + { name: 'onscroll' }, + { name: 'onsecuritypolicyviolation' }, + { name: 'onseeked' }, + { name: 'onseeking' }, + { name: 'onselect' }, + { name: 'onselectionchange' }, + { name: 'onselectstart' }, + { name: 'onslotchange' }, + { name: 'onstalled' }, + { name: 'onsubmit' }, + { name: 'onsuspend' }, + { name: 'ontimeupdate' }, + { name: 'ontoggle' }, + { name: 'ontouchcancel' }, + { name: 'ontouchend' }, + { name: 'ontouchmove' }, + { name: 'ontouchstart' }, + { name: 'ontransitioncancel' }, + { name: 'ontransitionend' }, + { name: 'ontransitionrun' }, + { name: 'ontransitionstart' }, + { name: 'onvolumechange' }, + { name: 'onwaiting' }, + { name: 'onwebkitanimationend' }, + { name: 'onwebkitanimationiteration' }, + { name: 'onwebkitanimationstart' }, + { name: 'onwebkittransitionend' }, + { name: 'onwheel' } +]; + +/** @type {{ name: string, description?: string }[]} */ +export const svelteEvents = [ + ...globalEvents, + { + name: 'onintrostart', + description: 'Available when element has transition' + }, + { + name: 'onintroend', + description: 'Available when element has transition' + }, + { + name: 'onoutrostart', + description: 'Available when element has transition' + }, + { + name: 'onoutroend', + description: 'Available when element has transition' + } +]; + +export const svelteAttributes = [ + { + name: 'bind:innerHTML', + description: 'Available when contenteditable=true' + }, + { + name: 'bind:textContent', + description: 'Available when contenteditable=true' + }, + { + name: 'bind:innerText', + description: 'Available when contenteditable=true' + }, + { + name: 'bind:clientWidth', + description: 'Available on all visible elements. (read-only)' + }, + { + name: 'bind:clientHeight', + description: 'Available on all visible elements. (read-only)' + }, + { + name: 'bind:offsetWidth', + description: 'Available on all visible elements. (read-only)' + }, + { + name: 'bind:offsetHeight', + description: 'Available on all visible elements. (read-only)' + }, + { + name: 'bind:this', + description: + 'To get a reference to a DOM node, use bind:this. If used on a component, gets a reference to that component instance.' + } +]; + +export const sveltekitAttributes = [ + { + name: 'data-sveltekit-keepfocus', + description: + 'SvelteKit-specific attribute. Currently focused element will retain focus after navigation. Otherwise, focus will be reset to the body.', + valueSet: 'v', + values: [{ name: 'off' }] + }, + { + name: 'data-sveltekit-noscroll', + description: + 'SvelteKit-specific attribute. Will prevent scrolling after the link is clicked.', + valueSet: 'v', + values: [{ name: 'off' }] + }, + { + name: 'data-sveltekit-preload-code', + description: + "SvelteKit-specific attribute. Will cause SvelteKit to run the page's load function as soon as the user hovers over the link (on a desktop) or touches it (on mobile), rather than waiting for the click event to trigger navigation.", + valueSet: 'v', + values: [ + { name: 'eager' }, + { name: 'viewport' }, + { name: 'hover' }, + { name: 'tap' }, + { name: 'off' } + ] + }, + { + name: 'data-sveltekit-preload-data', + description: + "SvelteKit-specific attribute. Will cause SvelteKit to run the page's load function as soon as the user hovers over the link (on a desktop) or touches it (on mobile), rather than waiting for the click event to trigger navigation.", + valueSet: 'v', + values: [{ name: 'hover' }, { name: 'tap' }, { name: 'off' }] + }, + { + name: 'data-sveltekit-reload', + description: + 'SvelteKit-specific attribute. Will cause SvelteKit to do a normal browser navigation which results in a full page reload.', + valueSet: 'v', + values: [{ name: 'off' }] + }, + { + name: 'data-sveltekit-replacestate', + description: + 'SvelteKit-specific attribute. Will replace the current `history` entry rather than creating a new one with `pushState` when the link is clicked.', + valueSet: 'v', + values: [{ name: 'off' }] + } +]; + +export const svelteTags = [ + { + name: 'svelte:self', + description: + 'Allows a component to include itself, recursively.\n\nIt cannot appear at the top level of your markup; it must be inside an if or each block to prevent an infinite loop.', + attributes: [] + }, + { + name: 'svelte:component', + description: + 'Renders a component dynamically, using the component constructor specified as the this property. When the property changes, the component is destroyed and recreated.\n\nIf this is falsy, no component is rendered.', + attributes: [ + { + name: 'this', + description: + 'Component to render.\n\nWhen this property changes, the component is destroyed and recreated.\nIf this is falsy, no component is rendered.' + } + ] + }, + { + name: 'svelte:element', + description: + 'Renders a DOM element dynamically, using the string as the this property. When the property changes, the element is destroyed and recreated.\n\nIf this is falsy, no element is rendered.', + attributes: [ + { + name: 'this', + description: + 'DOM element to render.\n\nWhen this property changes, the element is destroyed and recreated.\nIf this is falsy, no element is rendered.' + } + ] + }, + { + name: 'svelte:window', + description: + 'Allows you to add event listeners to the window object without worrying about removing them when the component is destroyed, or checking for the existence of window when server-side rendering.', + attributes: [ + { + name: 'bind:innerWidth', + description: 'Bind to the inner width of the window. (read-only)' + }, + { + name: 'bind:innerHeight', + description: 'Bind to the inner height of the window. (read-only)' + }, + { + name: 'bind:outerWidth', + description: 'Bind to the outer width of the window. (read-only)' + }, + { + name: 'bind:outerHeight', + description: 'Bind to the outer height of the window. (read-only)' + }, + { + name: 'bind:scrollX', + description: 'Bind to the scroll x position of the window.' + }, + { + name: 'bind:scrollY', + description: 'Bind to the scroll y position of the window.' + }, + { + name: 'bind:online', + description: 'An alias for window.navigator.onLine' + }, + // window events + { name: 'onafterprint' }, + { name: 'onbeforeprint' }, + { name: 'onbeforeunload' }, + { name: 'ongamepadconnected' }, + { name: 'ongamepaddisconnected' }, + { name: 'onhashchange' }, + { name: 'onlanguagechange' }, + { name: 'onmessage' }, + { name: 'onmessageerror' }, + { name: 'onoffline' }, + { name: 'ononline' }, + { name: 'onpagehide' }, + { name: 'onpageshow' }, + { name: 'onpopstate' }, + { name: 'onrejectionhandled' }, + { name: 'onstorage' }, + { name: 'onunhandledrejection' }, + { name: 'onunload' } + ] + }, + { + name: 'svelte:document', + description: + "As with , this element allows you to add listeners to events on document, such as visibilitychange, which don't fire on window.", + attributes: [ + // document events + { name: 'onfullscreenchange' }, + { name: 'onfullscreenerror' }, + { name: 'onpointerlockchange' }, + { name: 'onpointerlockerror' }, + { name: 'onreadystatechange' }, + { name: 'onvisibilitychange' } + ] + }, + { + name: 'svelte:body', + description: + "As with , this element allows you to add listeners to events on document.body, such as mouseenter and mouseleave which don't fire on window.", + attributes: [] + }, + { + name: 'svelte:head', + description: + 'This element makes it possible to insert elements into document.head. During server-side rendering, head content exposed separately to the main html content.', + attributes: [] + }, + { + name: 'svelte:options', + description: 'Provides a place to specify per-component compiler options', + attributes: [ + { + name: 'immutable', + description: + 'If true, tells the compiler that you promise not to mutate any objects. This allows it to be less conservative about checking whether values have changed.', + values: [ + { + name: '{true}', + description: + 'You never use mutable data, so the compiler can do simple referential equality checks to determine if values have changed' + }, + { + name: '{false}', + description: + 'The default. Svelte will be more conservative about whether or not mutable objects have changed' + } + ] + }, + { + name: 'accessors', + description: + "If true, getters and setters will be created for the component's props. If false, they will only be created for readonly exported values (i.e. those declared with const, class and function). If compiling with customElement: true this option defaults to true.", + values: [ + { + name: '{true}', + description: "Adds getters and setters for the component's props" + }, + { + name: '{false}', + description: 'The default.' + } + ] + }, + { + name: 'namespace', + description: 'The namespace where this component will be used, most commonly "svg"' + }, + { + name: 'tag', + description: 'The name to use when compiling this component as a custom element' + } + ] + }, + { + name: 'svelte:fragment', + description: + 'This element is useful if you want to assign a component to a named slot without creating a wrapper DOM element.', + attributes: [ + { + name: 'slot', + description: 'The name of the named slot that should be targeted.' + } + ] + }, + { + name: 'slot', + description: + 'Components can have child content, in the same way that elements can.\n\nThe content is exposed in the child component using the element, which can contain fallback content that is rendered if no children are provided.', + attributes: [ + { + name: 'name', + description: + 'Named slots allow consumers to target specific areas. They can also have fallback content.' + } + ] + } +]; + +const mediaAttributes = [ + { + name: 'bind:duration', + description: 'The total duration of the video, in seconds. (readonly)' + }, + { + name: 'bind:buffered', + description: 'An array of {start, end} objects. (readonly)' + }, + { + name: 'bind:seekable', + description: 'An array of {start, end} objects. (readonly)' + }, + { + name: 'bind:played', + description: 'An array of {start, end} objects. (readonly)' + }, + { + name: 'bind:seeking', + description: 'boolean. (readonly)' + }, + { + name: 'bind:ended', + description: 'boolean. (readonly)' + }, + { + name: 'bind:currentTime', + description: 'The current point in the video, in seconds.' + }, + { + name: 'bind:playbackRate', + description: "how fast or slow to play the video, where 1 is 'normal'" + }, + { + name: 'bind:paused' + }, + { + name: 'bind:volume', + description: 'A value between 0 and 1' + }, + { + name: 'bind:muted' + }, + { + name: 'bind:readyState' + } +]; + +const videoAttributes = [ + { + name: 'bind:videoWidth', + description: 'readonly' + }, + { + name: 'bind:videoHeight', + description: 'readonly' + } +]; + +const indeterminateAttribute = { + name: 'indeterminate', + description: 'Available for type="checkbox"' +}; + +/** @type {Record} */ +export const addAttributes = { + select: [{ name: 'bind:value' }], + input: [ + { name: 'bind:value' }, + { name: 'bind:group', description: 'Available for type="radio" and type="checkbox"' }, + { name: 'bind:checked', description: 'Available for type="checkbox"' }, + { name: 'bind:files', description: 'Available for type="file" (readonly)' }, + indeterminateAttribute, + { ...indeterminateAttribute, name: 'bind:indeterminate' } + ], + img: [{ name: 'bind:naturalWidth' }, { name: 'bind:naturalHeight' }], + textarea: [{ name: 'bind:value' }], + video: [...mediaAttributes, ...videoAttributes], + audio: [...mediaAttributes], + details: [ + { + name: 'bind:open' + } + ] +}; + +/** + * Returns `true` is this is a valid place to declare state + * @type {import("./types").Test} + */ +const is_state = (node) => { + let parent = node.parent; + + if (node.name === '.' || node.name === 'PropertyName') { + if (parent?.name !== 'MemberExpression') return false; + parent = parent.parent; + } + + if (!parent) return false; + + return parent.name === 'VariableDeclaration' || parent.name === 'PropertyDeclaration'; +}; + +/** + * Returns `true` if we're already in a valid call expression, e.g. + * changing an existing `$state()` to `$state.raw()` + * @type {import("./types").Test} + */ +const is_state_call = (node) => { + let parent = node.parent; + + if (node.name === '.' || node.name === 'PropertyName') { + if (parent?.name !== 'MemberExpression') return false; + parent = parent.parent; + } + + if (parent?.name !== 'CallExpression') { + return false; + } + + parent = parent.parent; + if (!parent) return false; + + return parent.name === 'VariableDeclaration' || parent.name === 'PropertyDeclaration'; +}; + +/** @type {import("./types").Test} */ +const is_statement = (node) => { + if (node.name === 'VariableName') { + return node.parent?.name === 'ExpressionStatement'; + } + + if (node.name === '.' || node.name === 'PropertyName') { + return node.parent?.parent?.name === 'ExpressionStatement'; + } + + return false; +}; + +/** + * Returns `true` if `$props()` is valid + * TODO only allow in `.svelte` files, and only at the top level + * @type {import("./types").Test} + */ +const is_props = (node, _, selected) => { + if (!selected.endsWith('.svelte')) return false; + + return ( + node.name === 'VariableName' && + node.parent?.name === 'VariableDeclaration' && + node.parent.parent?.name === 'Script' + ); +}; + +/** + * Returns `true` if `$bindable()` is valid + * @type {import("./types").Test} + * */ +const is_bindable = (node, context) => { + // disallow outside `let { x = $bindable }` + if (node.parent?.name !== 'PatternProperty') return false; + if (node.parent.parent?.name !== 'ObjectPattern') return false; + if (node.parent.parent.parent?.name !== 'VariableDeclaration') return false; + + let last = node.parent.parent.parent.lastChild; + if (!last) return true; + + // if the declaration is incomplete, assume the best + if (last.name === 'ObjectPattern' || last.name === 'Equals' || last.name === '⚠') { + return true; + } + + if (last.name === ';') { + last = last.prevSibling; + if (!last || last.name === '⚠') return true; + } + + // if the declaration is complete, only return true if it is a `$props()` declaration + return ( + last.name === 'CallExpression' && + last.firstChild?.name === 'VariableName' && + context.state.sliceDoc(last.firstChild.from, last.firstChild.to) === '$props' + ); +}; + +/** + * @type {import("./types").Test} + */ +const is_props_id_call = (node, context, selected) => { + return ( + is_state_call(node, context, selected) && + node.parent?.parent?.parent?.parent?.name === 'Script' + ); +}; + +export const runes = [ + { snippet: '$state(${})', test: is_state }, + { snippet: '$state', test: is_state_call }, + { snippet: '$props()', test: is_props }, + { snippet: '$props.id', test: is_props_id_call }, + { snippet: '$props.id()', test: is_props }, + { snippet: '$derived(${});', test: is_state }, + { snippet: '$derived', test: is_state_call }, + { snippet: '$derived.by(() => {\n\t${}\n});', test: is_state }, + { snippet: '$derived.by', test: is_state_call }, + { snippet: '$effect(() => {\n\t${}\n});', test: is_statement }, + { snippet: '$effect.pre(() => {\n\t${}\n});', test: is_statement }, + { snippet: '$state.raw(${});', test: is_state }, + { snippet: '$state.raw', test: is_state_call }, + { snippet: '$bindable()', test: is_bindable }, + { snippet: '$effect.root(() => {\n\t${}\n})' }, + { snippet: '$state.snapshot(${})' }, + { snippet: '$effect.tracking()' }, + { snippet: '$inspect(${});', test: is_statement }, + { snippet: '$inspect.trace();', test: is_statement }, + { snippet: '$inspect.trace(${});', test: is_statement }, + { snippet: '$host()' } +]; diff --git a/packages/repl/src/lib/site-kit/codemirror/index.js b/packages/repl/src/lib/site-kit/codemirror/index.js new file mode 100644 index 000000000..e8b9d7c93 --- /dev/null +++ b/packages/repl/src/lib/site-kit/codemirror/index.js @@ -0,0 +1,342 @@ +import { svelteLanguage } from '@replit/codemirror-lang-svelte'; +import { javascriptLanguage } from '@codemirror/lang-javascript'; +import { syntaxTree } from '@codemirror/language'; +import { CompletionContext, snippetCompletion } from '@codemirror/autocomplete'; +import { + addAttributes, + svelteAttributes, + svelteTags, + sveltekitAttributes, + svelteEvents, + runes +} from './autocompletionDataProvider.js'; + +const logic_block_snippets = [ + snippetCompletion('#if ${}}\n\n{/if', { label: '#if', type: 'keyword' }), + snippetCompletion('#each ${} as }\n\n{/each', { + label: '#each', + type: 'keyword' + }), + snippetCompletion('#await ${} then }\n\n{/await', { + label: '#await then', + type: 'keyword' + }), + snippetCompletion('#await ${}}\n\n{:then }\n\n{/await', { + label: '#await :then', + type: 'keyword' + }), + snippetCompletion('#key ${}}\n\n{/key', { label: '#key', type: 'keyword' }), + snippetCompletion('#snippet ${}()}\n\n{/snippet', { + label: '#snippet', + type: 'keyword' + }) +]; + +const special_tag_snippets = [ + snippetCompletion('@html ${}', { label: '@html', type: 'keyword' }), + snippetCompletion('@debug ${}', { label: '@debug', type: 'keyword' }), + snippetCompletion('@const ${}', { label: '@const', type: 'keyword' }), + snippetCompletion('@render ${}()', { label: '@render', type: 'keyword' }) +]; + +/** + * @param {import('@codemirror/autocomplete').CompletionContext} context + * @param {import("@lezer/common").SyntaxNode} node + */ +function completion_for_block(context, node) { + const prefix = context.state.doc.sliceString(node.from, node.from + 1); + + const from = node.from; + const to = context.pos; + + const type = 'keyword'; + + if (prefix === '/') { + /** @param {string} label */ + const completion = (label) => ({ + from, + to, + options: [{ label, type }], + validFor: /^\/\w*$/ + }); + + const parent = node.parent; + const block = node.parent?.parent; + + if (parent?.name === 'EachBlockClose' || block?.name === 'EachBlock') { + return completion('/each'); + } else if (parent?.name === 'IfBlockClose' || block?.name === 'IfBlock') { + return completion('/if'); + } else if (parent?.name === 'AwaitBlockClose' || block?.name === 'AwaitBlock') { + return completion('/await'); + } else if (parent?.name === 'KeyBlockClose' || block?.name === 'KeyBlock') { + return completion('/key'); + } + } else if (prefix === ':') { + /** @param {import('@codemirror/autocomplete').Completion[]} options */ + const completion = (options) => ({ from, to, options, validFor: /^\:\w*$/ }); + + const parent = node.parent; + const block = node.parent?.parent; + + if (parent?.name === 'ElseBlock' || block?.name === 'IfBlock') { + return completion([ + { label: ':else', type }, + { label: ':else if ', type } + ]); + } else if (parent?.name === 'ThenBlock' || block?.name === 'AwaitBlock') { + return completion([ + { label: ':then', type }, + { label: ':catch', type } + ]); + } + } else if (prefix === '#') { + return { from, to, options: logic_block_snippets, validFor: /^#(\w)*$/ }; + } else if (prefix === '@') { + return { from, to, options: special_tag_snippets, validFor: /^@(\w)*$/ }; + } + + return null; +} + +const options_for_svelte_events = [ + ...svelteEvents.map((e) => ({ ...e, boost: 1, name: e.name.replace(':', '') })), + ...svelteEvents +].map((event) => + snippetCompletion(event.name + '={${}}', { + label: event.name, + info: event.description, + type: 'keyword', + // @ts-ignore + boost: event.boost || 0 + }) +); + +const options_for_sveltekit_attributes = sveltekitAttributes.map((attr) => ({ + label: attr.name, + info: attr.description, + type: 'keyword' +})); + +const options_for_sveltekit_attr_values = sveltekitAttributes.reduce((prev, cur) => { + prev[cur.name] = cur.values.map((value) => ({ label: value.name, type: 'keyword' })); + return prev; +}, /** @type {Record} */ ({})); + +/** + * @param {{ name: string, description?: string}[]} attributes + */ +function snippet_for_attribute(attributes) { + return attributes.map((attr) => + snippetCompletion(attr.name + '={${}}', { + label: attr.name, + info: attr.description, + type: 'keyword' + }) + ); +} + +const options_for_svelte_attributes = snippet_for_attribute(svelteAttributes); + +const options_for_svelte_tags = svelteTags.reduce((tags, tag) => { + tags[tag.name] = snippet_for_attribute(tag.attributes); + return tags; +}, /** @type {Record} */ ({})); + +/** + * @param {import('@codemirror/autocomplete').CompletionContext} context + * @param {import("@lezer/common").SyntaxNode} node + */ +function completion_for_attributes(context, node) { + /** @param {import('@codemirror/autocomplete').Completion[]} options */ + const completion = (options) => { + return { from: node.from, to: context.pos, options, validFor: /^\w*$/ }; + }; + + const global_options = [ + ...options_for_svelte_events, + ...options_for_svelte_attributes, + ...options_for_sveltekit_attributes + ]; + + if (node.parent?.parent?.firstChild?.nextSibling?.name === 'SvelteElementName') { + const tag_node = node.parent.parent.firstChild.nextSibling; + const tag = context.state.doc.sliceString(tag_node.from, tag_node.to); + return completion([...global_options, ...options_for_svelte_tags[tag]]); + } else if (node.parent?.parent?.firstChild?.nextSibling?.name === 'TagName') { + const tag_node = node.parent.parent.firstChild.nextSibling; + const tag = context.state.doc.sliceString(tag_node.from, tag_node.to); + if (addAttributes[tag]) { + const completions_attributes = snippet_for_attribute(addAttributes[tag]); + return completion([...global_options, ...completions_attributes]); + } + } + + return completion(global_options); +} + +/** + * @param {import('@codemirror/autocomplete').CompletionContext} context + * @param {import("@lezer/common").SyntaxNode} node + * @param {string} attr + */ +function completion_for_sveltekit_attr_values(context, node, attr) { + const options = options_for_sveltekit_attr_values[attr]; + if (options) { + return { + from: node.name === 'AttributeValueContent' ? node.from : node.from + 1, + to: context.pos, + options, + validFor: /^\w*$/ + }; + } + + return null; +} + +/** + * @param {import('@codemirror/autocomplete').CompletionContext} context + * @returns {import('@codemirror/autocomplete').CompletionResult | null} + */ +function completion_for_markup(context) { + const node_before = syntaxTree(context.state).resolveInner(context.pos, -1); + + if (node_before.name === 'BlockPrefix') { + return completion_for_block(context, node_before); + } else if (node_before.prevSibling?.name === 'BlockPrefix') { + return completion_for_block(context, node_before.prevSibling); + } else if (node_before.name === 'AttributeName') { + return completion_for_attributes(context, node_before); + } else if ( + node_before.name === 'DirectiveOn' || + node_before.name === 'DirectiveBind' || + node_before.name === 'DirectiveTarget' + ) { + if (node_before.parent) { + return completion_for_attributes(context, node_before.parent); + } + } else if (node_before.parent?.name === 'AttributeValue') { + if (node_before.parent?.parent?.firstChild) { + const attr_name_node = node_before.parent.parent.firstChild; + const attr_name = context.state.doc.sliceString(attr_name_node.from, attr_name_node.to); + + if (attr_name.startsWith('data-sveltekit-')) { + return completion_for_sveltekit_attr_values(context, node_before, attr_name); + } + } + } + + return null; +} + +const options = runes.map(({ snippet, test }, i) => ({ + option: snippetCompletion(snippet, { + type: 'keyword', + boost: runes.length - i, + label: snippet.includes('(') ? snippet.slice(0, snippet.indexOf('(')) : snippet + }), + test +})); + +/** + * @param {import('@codemirror/autocomplete').CompletionContext} context + * @param {string} selected + * @param {string[]} files + * @returns {import('@codemirror/autocomplete').CompletionResult | null | false} + */ +export function completion_for_javascript(context, selected, files) { + let node = syntaxTree(context.state).resolveInner(context.pos, -1); + + if (node.name === 'String' && node.parent?.name === 'ImportDeclaration') { + const modules = [ + 'svelte', + 'svelte/animate', + 'svelte/easing', + 'svelte/legacy', + 'svelte/motion', + 'svelte/reactivity', + 'svelte/reactivity/window', + 'svelte/store', + 'svelte/transition' + ]; + + for (const file of files) { + if (file === selected) continue; + + const from = selected.split('/'); + const to = file.split('/'); + + while (from[0] === to[0]) { + from.shift(); + to.shift(); + } + + const prefix = from.length === 1 ? './' : '../'.repeat(from.length - 1); + modules.push(prefix + to.join('/')); + } + + return { + from: node.from + 1, + options: modules.map((label) => ({ + label, + type: 'string' + })) + }; + } + + if (!selected.endsWith('.svelte.js') && !selected.endsWith('.svelte')) { + return false; + } + + if (node.name === 'VariableName' || node.name === 'PropertyName' || node.name === '.') { + // special case — `$inspect(...).with(...)` is the only rune that 'returns' + // an 'object' with a 'method' + if (node.name === 'PropertyName' || node.name === '.') { + if ( + node.parent?.name === 'MemberExpression' && + node.parent.firstChild?.name === 'CallExpression' && + node.parent.firstChild.firstChild?.name === 'VariableName' && + context.state.sliceDoc( + node.parent.firstChild.firstChild.from, + node.parent.firstChild.firstChild.to + ) === '$inspect' + ) { + const open = context.matchBefore(/\.\w*/); + if (!open) return null; + + return { + from: open.from, + options: [snippetCompletion('.with(${})', { type: 'keyword', label: '.with' })] + }; + } + } + + const open = context.matchBefore(/\$[\w\.]*/); + if (!open) return null; + + return { + from: open.from, + options: options + .filter((option) => (option.test ? option.test(node, context, selected) : true)) + .map((option) => option.option) + }; + } + + return null; +} + +/** + * @param {() => string} selected + * @param {() => string[]} files + */ +export function autocomplete_for_svelte(selected, files) { + return [ + svelteLanguage.data.of({ + autocomplete: completion_for_markup + }), + javascriptLanguage.data.of({ + autocomplete: (/** @type {CompletionContext} */ context) => + completion_for_javascript(context, selected(), files()) + }) + ]; +} diff --git a/packages/repl/src/lib/site-kit/codemirror/types.d.ts b/packages/repl/src/lib/site-kit/codemirror/types.d.ts new file mode 100644 index 000000000..09bf8bca6 --- /dev/null +++ b/packages/repl/src/lib/site-kit/codemirror/types.d.ts @@ -0,0 +1,9 @@ +import { CompletionContext, snippetCompletion } from '@codemirror/autocomplete'; +import type { SyntaxNode } from '@lezer/common'; +import type { File } from './types'; + +export interface Test { + (node: SyntaxNode, context: CompletionContext, selected: File): boolean; +} + +export type { File, SyntaxNode }; diff --git a/packages/repl/src/lib/site-kit/components/Checkbox.svelte b/packages/repl/src/lib/site-kit/components/Checkbox.svelte new file mode 100644 index 000000000..a5495bd92 --- /dev/null +++ b/packages/repl/src/lib/site-kit/components/Checkbox.svelte @@ -0,0 +1,74 @@ + + + onchange?.(e.currentTarget.checked)} /> + + diff --git a/packages/repl/src/lib/site-kit/components/Dropdown.svelte b/packages/repl/src/lib/site-kit/components/Dropdown.svelte new file mode 100644 index 000000000..224339d07 --- /dev/null +++ b/packages/repl/src/lib/site-kit/components/Dropdown.svelte @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/repl/src/lib/site-kit/components/HoverMenu.svelte b/packages/repl/src/lib/site-kit/components/HoverMenu.svelte new file mode 100644 index 000000000..3e1257cb6 --- /dev/null +++ b/packages/repl/src/lib/site-kit/components/HoverMenu.svelte @@ -0,0 +1,47 @@ + + +
    + {@render children()} +
    + + diff --git a/packages/repl/src/lib/site-kit/components/ScreenToggle.svelte b/packages/repl/src/lib/site-kit/components/ScreenToggle.svelte new file mode 100644 index 000000000..1688a7f4b --- /dev/null +++ b/packages/repl/src/lib/site-kit/components/ScreenToggle.svelte @@ -0,0 +1,51 @@ + + + + + + diff --git a/packages/repl/src/lib/site-kit/components/Toolbox.svelte b/packages/repl/src/lib/site-kit/components/Toolbox.svelte new file mode 100644 index 000000000..8411cbdd2 --- /dev/null +++ b/packages/repl/src/lib/site-kit/components/Toolbox.svelte @@ -0,0 +1,54 @@ + + + +
    + + + + + +
    + + {#snippet dropdown()} + + {@render children()} + + {/snippet} +
    + + diff --git a/packages/repl/src/lib/site-kit/components/index.ts b/packages/repl/src/lib/site-kit/components/index.ts new file mode 100644 index 000000000..283996abc --- /dev/null +++ b/packages/repl/src/lib/site-kit/components/index.ts @@ -0,0 +1,5 @@ +export { default as Checkbox } from './Checkbox.svelte'; +export { default as Dropdown } from './Dropdown.svelte'; +export { default as HoverMenu } from './HoverMenu.svelte'; +export { default as ScreenToggle } from './ScreenToggle.svelte'; +export { default as Toolbox } from './Toolbox.svelte'; diff --git a/packages/repl/src/lib/site-kit/polyfills/index.ts b/packages/repl/src/lib/site-kit/polyfills/index.ts new file mode 100644 index 000000000..4a9c915c2 --- /dev/null +++ b/packages/repl/src/lib/site-kit/polyfills/index.ts @@ -0,0 +1,16 @@ +if (!Array.prototype.at) { + Array.prototype.at = function (index: number) { + return this[index >= 0 ? index : this.length + index]; + }; +} + +if (!Promise.withResolvers) { + (Promise as any).withResolvers = function () { + let resolve: any, reject: any; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { resolve, reject, promise }; + }; +} diff --git a/packages/repl/src/lib/theme.ts b/packages/repl/src/lib/theme.ts new file mode 100644 index 000000000..dea9cafd4 --- /dev/null +++ b/packages/repl/src/lib/theme.ts @@ -0,0 +1,55 @@ +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { tags as t } from '@lezer/highlight'; + +export const theme = syntaxHighlighting( + HighlightStyle.define([ + { tag: t.keyword, color: 'var(--shiki-token-keyword)' }, + { + tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], + color: 'var(--shiki-color-text)' + }, + { tag: [t.function(t.variableName), t.labelName], color: 'var(--shiki-token-function)' }, + { + tag: [t.color, t.constant(t.name), t.standard(t.name)], + color: 'var(--shiki-color-text)' + }, + { tag: [t.definition(t.name), t.separator], color: 'var(--shiki-color-text)' }, + { + tag: [ + t.typeName, + t.className, + t.number, + t.changed, + t.annotation, + t.modifier, + t.self, + t.namespace + ], + color: 'var(--shiki-token-function)' + }, + { + tag: [ + t.operator, + t.operatorKeyword, + t.url, + t.escape, + t.regexp, + t.link, + t.special(t.string) + ], + color: 'var(--shiki-color-text)' + }, + { tag: [t.meta, t.comment], color: 'var(--shiki-token-comment)' }, + { tag: t.strong, fontWeight: 'bold' }, + { tag: t.emphasis, fontStyle: 'italic' }, + { tag: t.strikethrough, textDecoration: 'line-through' }, + { tag: t.link, color: 'var(--shiki-color-text)', textDecoration: 'underline' }, + { tag: t.heading, fontWeight: 'bold', color: 'var(--sk-fg-1)' }, + { tag: [t.atom, t.bool], color: 'var(--sk-code-atom)' }, + { + tag: [t.processingInstruction, t.string, t.inserted], + color: 'var(--shiki-token-string)' + }, + { tag: t.invalid, color: '#ff008c' } + ]) +); diff --git a/packages/repl/src/lib/types.d.ts b/packages/repl/src/lib/types.d.ts new file mode 100644 index 000000000..746c0aaa8 --- /dev/null +++ b/packages/repl/src/lib/types.d.ts @@ -0,0 +1,43 @@ +import type { EditorState } from '@codemirror/state'; +import { OutputChunk, RollupError } from '@rollup/browser'; +import type { Readable, Writable } from 'svelte/store'; +import type { CompileError } from 'svelte/compiler'; +import type { Workspace } from './Workspace.svelte'; +import type { BundleResult } from './public'; + +export type Lang = 'js' | 'svelte' | 'json' | 'md' | 'css' | (string & Record); + +type StartOrEnd = { + line: number; + column: number; + character: number; +}; + +export type MessageDetails = { + start: StartOrEnd; + end: StartOrEnd; + filename: string; + message: string; +}; + +export type Warning = MessageDetails; + +export type File = { + name: string; + source: string; + type: Lang; + modified?: boolean; +}; + +export type ReplState = { + bundle: BundleResult | null; + bundler: import('./Bundler.svelte').default | null; + toggleable: boolean; +}; + +export type ReplContext = { + bundler: Bundler; + toggleable: Writable; + workspace: Workspace; + svelteVersion: string; +}; diff --git a/packages/repl/src/lib/utils.ts b/packages/repl/src/lib/utils.ts new file mode 100644 index 000000000..4c4a03c1a --- /dev/null +++ b/packages/repl/src/lib/utils.ts @@ -0,0 +1,4 @@ +export const clamp = (min: number, max: number, value: number) => + Math.max(min, Math.min(max, value)); + +export const sleep = (ms: number) => new Promise((f) => setTimeout(f, ms)); diff --git a/packages/repl/src/lib/workers/bundler/index.ts b/packages/repl/src/lib/workers/bundler/index.ts new file mode 100644 index 000000000..5094d4ff9 --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/index.ts @@ -0,0 +1,691 @@ +import '@sveltejs/site-kit/polyfills'; +import { walk } from 'zimmerframe'; +import '../patch_window'; +import { rollup } from '@rollup/browser'; +import { DEV } from 'esm-env'; +import typescript_strip_types from './plugins/typescript'; +import commonjs from './plugins/commonjs'; +import glsl from './plugins/glsl'; +import json from './plugins/json'; +import csv from './plugins/csv'; +import mp3 from './plugins/mp3'; +import image from './plugins/image'; +import svg from './plugins/svg'; +import replace from './plugins/replace'; +import loop_protect from './plugins/loop-protect'; +import alias_plugin, { resolve } from './plugins/alias'; +import type { Plugin, RollupCache, TransformResult } from '@rollup/browser'; +import type { BundleMessageData, BundleOptions } from '../workers'; +import type { Warning } from '../../types'; +import type { CompileError, CompileResult } from 'svelte/compiler'; +import type { File } from '../../Workspace.svelte'; +import type { Node } from 'estree'; +import { max } from './semver'; +import { NPM, VIRTUAL } from '../constants'; +import { + normalize_path, + fetch_package, + load_svelte, + parse_npm_url, + resolve_local, + resolve_subpath, + resolve_version, + type Package +} from '../npm'; +import type { BundleResult } from '$lib/public'; + +// hack for magic-string and rollup inline sourcemaps +// do not put this into a separate module and import it, would be treeshaken in prod +self.window = self; + +const ENTRYPOINT = '__entry.js'; +const WRAPPER = '__wrapper.svelte'; +const STYLES = '__styles.js'; +const ESM_ENV = '__esm-env.js'; + +let current_id: number; + +self.addEventListener('message', async (event: MessageEvent) => { + switch (event.data.type) { + case 'init': { + get_svelte(event.data.svelte_version); + break; + } + + case 'bundle': { + try { + const { uid, files, options } = event.data; + const { + svelte, + version: svelte_version, + can_use_experimental_async + } = await get_svelte(options.svelte_version); + + current_id = uid; + + setTimeout(async () => { + if (current_id !== uid) return; + + const use_async = can_use_experimental_async && options.async; + + const result = await bundle( + svelte, + svelte_version, + uid, + files, + options, + use_async + ); + + console.log('[bundle worker result]', result); + + // error object might be augmented, see https://github.com/rollup/rollup/blob/76a3b8ede4729a71eb522fc29f7d550a4358827b/docs/plugin-development/index.md#thiserror, + // hence only check that the specific abort property we set is there + if ( + (result.error as any)?.svelte_bundler_aborted === + ABORT.svelte_bundler_aborted + ) { + return; + } + if (result && uid === current_id) postMessage(result); + }); + } catch (e) { + self.postMessage({ + type: 'error', + uid: event.data.uid, + message: `Error loading the compiler: ${(e as Error).message}` + }); + } + + break; + } + } +}); + +let ready: ReturnType; +let ready_version: string; + +function get_svelte(svelte_version: string) { + if (ready_version === svelte_version) return ready; + + self.postMessage({ type: 'status', message: `fetching svelte@${svelte_version}` }); + ready_version = svelte_version; + ready = load_svelte(svelte_version || 'latest'); + ready.then(({ version, can_use_experimental_async }) => { + ready_version = version; + self.postMessage({ + type: 'version', + version, + supports_async: can_use_experimental_async + }); + }); + return ready; +} + +const ABORT = { svelte_bundler_aborted: true }; + +let previous: { + key: string; + cache: RollupCache | undefined; + /** Needed because if rollup cache hits then we won't be able to pick up all candidates in subsequent runs */ + tailwind_candidates: Set; +}; + +let tailwind: Awaited>; + +async function init_tailwind(user_css = '') { + const tailwindcss = await import('tailwindcss'); + + const { default: tailwind_preflight } = await import('tailwindcss/preflight.css?raw'); + const { default: tailwind_theme } = await import('tailwindcss/theme.css?raw'); + const { default: tailwind_utilities } = await import('tailwindcss/utilities.css?raw'); + + const tailwind_files: Record = { + 'tailwindcss/theme.css': tailwind_theme, + 'tailwindcss/preflight.css': tailwind_preflight, + 'tailwindcss/utilities.css': tailwind_utilities + }; + + const tailwind_base = [ + `@layer theme, base, components, utilities;`, + `@import "tailwindcss/theme.css" layer(theme);`, + `@import "tailwindcss/preflight.css" layer(base);`, + `@import "tailwindcss/utilities.css" layer(utilities);`, + user_css + ].join('\n'); + + return await tailwindcss.compile(tailwind_base, { + loadStylesheet: async (id, base) => { + return { content: tailwind_files[id], base, path: '' }; + } + }); +} + +async function get_bundle( + svelte: typeof import('svelte/compiler'), + svelte_version: string, + uid: number, + mode: 'client' | 'server', + virtual: Map, + options: BundleOptions, + can_use_experimental_async: boolean +) { + let bundle; + + const key = JSON.stringify(options); + /** A set of package names (without subpaths) to include in pkg.devDependencies when downloading an app */ + const imports: Set = new Set(); + const warnings: Warning[] = []; + const all_warnings: Array<{ message: string }> = []; + + const tailwind_candidates = + previous?.key === key ? previous.tailwind_candidates : new Set(); + + function add_tailwind_candidates(ast: Node | undefined) { + if (!ast) return; + + walk(ast, null, { + ImportDeclaration() { + // don't descend into these nodes, so that we don't + // pick up import sources + }, + Literal(node) { + if (typeof node.value === 'string' && node.value) { + for (const candidate of node.value.split(' ')) { + if (candidate) tailwind_candidates.add(candidate); + } + } + }, + TemplateElement(node) { + if (node.value.raw) { + for (const candidate of node.value.raw.split(' ')) { + if (candidate) tailwind_candidates.add(candidate); + } + } + } + }); + } + + const repl_plugin: Plugin = { + name: 'svelte-repl', + async resolveId(importee, importer) { + if (uid !== current_id) throw ABORT; + + // entrypoint + if (!importer) return `${VIRTUAL}/${ENTRYPOINT}`; + + // special case + if (importee === 'esm-env') return `${VIRTUAL}/${ESM_ENV}`; + + // importing from a URL + if (/^[a-z]+:/.test(importee)) return importee; + + // importing an absolute path (e.g. /data/penguins.csv) — resolve against the origin + if (importee[0] === '/') { + return self.location.origin + importee; + } + + /** The npm package we're importing from, if any */ + let current: null | Package; + + if (importer.startsWith(NPM)) { + const { name, version } = parse_npm_url(importer); + current = await fetch_package(name, name === 'svelte' ? svelte_version : version); + } + + // importing a relative file + if (importee[0] === '.') { + if (importer.startsWith(VIRTUAL)) { + return resolve(virtual, importee, importer); + } + + if (current) { + const { name, version } = current.meta; + const path = new URL(importee, importer).href.replace( + `${NPM}/${name}@${version}/`, + '' + ); + + return normalize_path(current, path, importee, importer); + } + + return new URL(importee, importer).href; + } + + // importing a file from the same package via pkg.imports + if (importee[0] === '#') { + if (current) { + const subpath = resolve_subpath(current, importee); + return normalize_path(current, subpath.slice(2), importee, importer); + } + return await resolve_local(importee); + } + + // importing an external package -> `npm://$/@/` + const match = /^((?:@[^/]+\/)?[^/@]+)(?:@([^/]+))?(\/.+)?$/.exec(importee); + if (!match) throw new Error(`Invalid import "${importee}"`); + + const pkg_name = match[1]; + + if (pkg_name === 'svelte' && svelte_version === 'local') { + return await resolve_local(importee); + } + + let default_version = 'latest'; + + if (current) { + // use the version specified in importer's package.json, not `latest` + const { meta } = current; + + if (meta.name === pkg_name) { + default_version = meta.version; + } else { + default_version = max( + meta.devDependencies?.[pkg_name] ?? + meta.peerDependencies?.[pkg_name] ?? + meta.dependencies?.[pkg_name] + ); + } + } + + if (importer.startsWith(VIRTUAL)) { + // if this was imported by one of our files, add it to the `imports` set + imports.add(pkg_name); + } + + const v = await resolve_version(match[1], match[2] ?? default_version); + const pkg = await fetch_package(pkg_name, pkg_name === 'svelte' ? svelte_version : v); + const subpath = resolve_subpath(pkg, '.' + (match[3] ?? '')); + + return normalize_path(pkg, subpath.slice(2), importee, importer); + }, + async load(resolved) { + if (uid !== current_id) throw ABORT; + + if (resolved.startsWith(VIRTUAL)) { + const file = virtual.get(resolved.slice(VIRTUAL.length + 1))!; + return file.contents; + } + + if (resolved.startsWith(NPM)) { + let [, name, v, subpath] = + /^npm:\/\/\$\/((?:@[^/]+\/)?[^/@]+)(?:@([^/]+))?\/(.+)$/.exec(resolved)!; + + const pkg = await fetch_package(name, name === 'svelte' ? svelte_version : v); + + const file = pkg.contents[subpath]; + if (file) return file.text; + } + + const response = await fetch(resolved); + if (response.ok) return response.text(); + + throw new Error(`Could not load ${resolved}`); + }, + transform(code, id) { + if (uid !== current_id) throw ABORT; + + const name = id.replace(VIRTUAL + '/', '').replace(NPM + '/', ''); + + self.postMessage({ type: 'status', message: `bundling ${name}` }); + + if (!/\.(svelte|js|ts)$/.test(id)) return null; + + let result: CompileResult; + + if (id.endsWith('.svelte')) { + const is_gt_5 = Number(svelte.VERSION.split('.')[0]) >= 5; + + const compilerOptions: any = { + filename: name, + generate: is_gt_5 ? 'client' : 'dom', + dev: true, + fragments: options.fragments + }; + + if (is_gt_5) { + compilerOptions.runes = options.runes; + } + + if (can_use_experimental_async) { + compilerOptions.experimental = { async: true }; + } + + if (compilerOptions.fragments == null) { + // if fragments is not set it probably means we are using + // a version that doesn't support it, so we need to remove it + delete compilerOptions.fragments; + } + + result = svelte.compile(code, compilerOptions); + + walk(result.ast.html as import('svelte/compiler').AST.TemplateNode, null, { + Attribute(node) { + if (Array.isArray(node.value)) { + for (const chunk of node.value) { + if (chunk.type === 'Text') { + for (const candidate of chunk.data.split(' ')) { + if (candidate) tailwind_candidates.add(candidate); + } + } + } + } + } + }); + + add_tailwind_candidates(result.ast.module); + add_tailwind_candidates(result.ast.instance); + add_tailwind_candidates(result.ast.html); + + if (result.css?.code) { + // resolve local files by inlining them + result.css.code = result.css.code.replace( + /url\(['"]?\.\/(.+?\.(svg|webp|png))['"]?\)/g, + (match, $1, $2) => { + if (virtual.has($1)) { + if ($2 === 'svg') { + return `url('data:image/svg+xml;base64,${btoa(virtual.get($1)!.contents)}')`; + } else { + return `url('data:image/${$2};base64,${virtual.get($1)!.contents}')`; + } + } else { + return match; + } + } + ); + // add the CSS via injecting a style tag + result.js.code += + '\n\n' + + ` + import { styles as $$_styles } from '${VIRTUAL}/${STYLES}'; + const $$__style = document.createElement('style'); + $$__style.textContent = ${JSON.stringify(result.css.code)}; + document.head.append($$__style); + $$_styles.push($$__style); + `.replace(/\t/g, ''); + } + } else if (/\.svelte\.(js|ts)$/.test(id)) { + const compilerOptions: any = { + filename: name, + generate: 'client', + dev: true + }; + + if (can_use_experimental_async) { + compilerOptions.experimental = { async: true }; + } + + result = svelte.compileModule?.(code, compilerOptions); + + if (!result) { + return null; + } + } else { + return null; + } + + // @ts-expect-error + (result.warnings || result.stats?.warnings)?.forEach((warning) => { + // This is required, otherwise postMessage won't work + // @ts-ignore + delete warning.toString; + // TODO remove stats post-launch + // @ts-ignore + warnings.push(warning); + }); + + const transform_result: TransformResult = { + code: result.js.code, + map: result.js.map + }; + + return transform_result; + } + }; + + const handled_css_ids = new Set(); + let user_css = ''; + + bundle = await rollup({ + input: './__entry.js', + cache: previous?.key === key ? previous.cache : true, + plugins: [ + alias_plugin(options.aliases, virtual), + typescript_strip_types, + repl_plugin, + commonjs, + json, + csv, + svg, + mp3, + image, + glsl, + loop_protect, + replace({ + 'process.env.NODE_ENV': JSON.stringify('production') + }), + { + name: 'css', + transform(code, id) { + if (id.endsWith('.css')) { + if (!handled_css_ids.has(id)) { + handled_css_ids.add(id); + // We don't handle imports in the user CSS right now, so we remove them + // to avoid errors in e.g. the Tailwind compiler + user_css += '\n' + code.replace(/@import\s+["'][^"']+["'][^;]*;/g, ''); + } + return { + code: '', + map: null + }; + } + } + }, + options.tailwind && { + name: 'tailwind-extract', + transform(code, id) { + // TODO tidy this up + if (id.startsWith(`${NPM}/svelte@`)) return; + if (id.startsWith(`${NPM}/clsx@`)) return; + if (id === `${VIRTUAL}/${ENTRYPOINT}`) return; + if (id === `${VIRTUAL}/${STYLES}`) return; + if (id === `${VIRTUAL}/${ESM_ENV}`) return; + if (id.endsWith('.svelte')) return; + + try { + // Don't let a parser/tailwind error crash the bundler + // Can happen for files that begin with a bash shebang + add_tailwind_candidates(this.parse(code)); + } catch {} + } + } + ], + onwarn(warning) { + all_warnings.push({ + message: warning.message + }); + } + }); + + previous = { key, cache: bundle.cache, tailwind_candidates }; + + return { + bundle, + css: options.tailwind + ? (tailwind ?? (await init_tailwind(user_css))).build([...tailwind_candidates]) + : user_css + ? user_css + : null, + imports: Array.from(imports), + error: null, + warnings, + all_warnings + }; +} + +async function bundle( + svelte: typeof import('svelte/compiler'), + svelte_version: string, + uid: number, + files: File[], + options: BundleOptions, + can_use_experimental_async: boolean +): Promise { + if (!DEV) { + console.log(`running Svelte compiler version %c${svelte.VERSION}`, 'font-weight: bold'); + } + + const lookup: Map = new Map(); + + lookup.set(ENTRYPOINT, { + type: 'file', + name: ENTRYPOINT, + basename: ENTRYPOINT, + contents: + svelte.VERSION.split('.')[0] >= '5' + ? ` + import { unmount as u } from 'svelte'; + import { styles } from '${VIRTUAL}/${STYLES}'; + export { mount, untrack } from 'svelte'; + export { default as App } from '${VIRTUAL}/${WRAPPER}'; + export function unmount(component) { + u(component); + styles.forEach(style => style.remove()); + } + ` + : ` + import { styles } from '${VIRTUAL}/${STYLES}'; + export { default as App } from './App.svelte'; + export function mount(component, options) { + return new component(options); + } + export function unmount(component) { + component.$destroy(); + styles.forEach(style => style.remove()); + } + export function untrack(fn) { + return fn(); + } + `, + text: true + }); + + const wrapper = can_use_experimental_async + ? ` + + + + + + {#snippet pending()}{/snippet} + + ` + : ` + + + + `; + + lookup.set(WRAPPER, { + type: 'file', + name: WRAPPER, + basename: WRAPPER, + contents: wrapper, + text: true + }); + + lookup.set(STYLES, { + type: 'file', + name: STYLES, + basename: STYLES, + contents: ` + export let styles = []; + `, + text: true + }); + + lookup.set(ESM_ENV, { + type: 'file', + name: STYLES, + basename: STYLES, + contents: ` + export const BROWSER = true; + export const DEV = true; + `, + text: true + }); + + files.forEach((file) => { + lookup.set(file.name, file); + }); + + try { + let client: Awaited> = await get_bundle( + svelte, + svelte_version, + uid, + 'client', + lookup, + options, + can_use_experimental_async + ); + + const client_result = ( + await client.bundle?.generate({ + format: 'iife', + exports: 'named', + inlineDynamicImports: true + // sourcemap: 'inline' + }) + )?.output[0]; + + const server = false // TODO how can we do SSR? + ? await get_bundle( + svelte, + svelte_version, + uid, + 'server', + lookup, + options, + can_use_experimental_async + ) + : null; + + const server_result = server + ? ( + await server.bundle?.generate({ + format: 'iife', + name: 'SvelteComponent', + exports: 'named' + // sourcemap: 'inline' + }) + )?.output?.[0] + : null; + + return { + uid, + error: null, + client: client_result, + server: server_result, + css: client.css, + imports: client.imports + }; + } catch (err) { + console.error(err); + + const e = err as CompileError; // TODO could be a non-Svelte error? + + return { + uid, + error: { ...e, message: e.message }, // not all Svelte versions return an enumerable message property + client: null, + server: null, + css: null, + imports: null + }; + } +} diff --git a/packages/repl/src/lib/workers/bundler/plugins/alias.ts b/packages/repl/src/lib/workers/bundler/plugins/alias.ts new file mode 100644 index 000000000..4aa97b8b9 --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/alias.ts @@ -0,0 +1,75 @@ +import { VIRTUAL } from '../../constants'; +import type { File } from '$lib/Workspace.svelte'; +import type { Plugin } from '@rollup/browser'; + +/** + * Alias plugin for resolving import aliases (e.g., $lib -> src/lib). + * This will also run on npm packages, so that e.g. SvelteKit libraries using $app/environment can also make use of this. + * + * @example + * // With aliases: { '$lib': 'src/lib' } + * // import foo from '$lib/foo' -> import foo from 'src/lib/foo' + * // import bar from '$lib' -> import bar from 'src/lib' + */ +function alias_plugin(aliases: Record = {}, virtual: Map): Plugin { + // Sort aliases by length (longest first) to avoid conflicts + const alias_entries = Object.entries(aliases).sort((a, b) => b[0].length - a[0].length); + + return { + name: 'alias', + resolveId(importee, importer) { + // Skip if no aliases configured / this is a relative import + if (alias_entries.length === 0 || importee[0] === '.') { + return null; + } + + // Check if the import matches any alias + for (const [alias_key, alias_path] of alias_entries) { + if (importee === alias_key) { + // Exact match - replace with alias path + return resolve(virtual, `${VIRTUAL}/${alias_path}`, importer); + } else if (importee.startsWith(alias_key + '/')) { + // Partial match - replace the prefix + const relative_path = importee.slice(alias_key.length + 1); + return resolve(virtual, `${VIRTUAL}/${alias_path}/${relative_path}`, importer); + } + } + + // No alias match, let other plugins handle it + return null; + } + }; +} + +/** + * Tries to resolve the import path based on the virtual file map, trying different suffixes. + */ +export function resolve(virtual: Map, importee: string, importer: string): string { + const url = new URL(importee, importer); + + for (const suffix of ['', '.js', '.json', '.ts', '/index.js', '/index.ts']) { + const with_suffix = `${url.href.slice(VIRTUAL.length + 1)}${suffix}`; + const file = virtual.get(with_suffix); + + if (file) { + return url.href + suffix; + } + } + + if (url.href.endsWith('.ts') || url.href.endsWith('.js')) { + // One can mean the other (TS encourages you to import .ts files with .js suffixes, and bundlers handle these cases) + const other_suffix = url.href.endsWith('.ts') ? '.js' : '.ts'; + const with_other_suffix = `${url.href.slice(VIRTUAL.length + 1, -3)}${other_suffix}`; + const file = virtual.get(with_other_suffix); + + if (file) { + return url.href.slice(0, -3) + other_suffix; + } + } + + throw new Error( + `'${importee}' (imported by ${importer.replace(VIRTUAL + '/', '')}) does not exist` + ); +} + +export default alias_plugin; diff --git a/packages/repl/src/lib/workers/bundler/plugins/commonjs.ts b/packages/repl/src/lib/workers/bundler/plugins/commonjs.ts new file mode 100644 index 000000000..b795b910c --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/commonjs.ts @@ -0,0 +1,97 @@ +import type { Plugin } from '@rollup/browser'; +import { parse } from 'acorn'; +import type { Node } from 'estree'; +import { walk } from 'zimmerframe'; + +const require = `function require(id) { + if (id in __repl_lookup) return __repl_lookup[id]; + throw new Error(\`Cannot require modules dynamically (\${id})\`); +}`; + +const plugin: Plugin = { + name: 'commonjs', + + transform: (code, id) => { + if ( + id.endsWith('.mjs') || + id.endsWith('.esm.js') || + code.startsWith('import ') || + !/\b(require|module|exports)\b/.test(code) + ) { + return; + } + + try { + const ast = parse(code, { + ecmaVersion: 'latest' + }); + + const requires: string[] = []; + const exports: Set = new Set(); + + walk(ast as Node, null, { + CallExpression: (node, context) => { + if (node.callee.type === 'Identifier' && node.callee.name === 'require') { + if (node.arguments.length !== 1) return; + const arg = node.arguments[0]; + if (arg.type !== 'Literal' || typeof arg.value !== 'string') return; + + requires.push(arg.value); + } + + context.next(); + }, + AssignmentExpression: (node, context) => { + // walk children to find nested requires + context.next(); + + if (node.operator !== '=') return; + if (node.left.type !== 'MemberExpression') return; + if ( + node.left.object.type !== 'Identifier' || + node.left.object.name !== 'exports' + ) + return; + if (node.left.computed || node.left.property.type !== 'Identifier') return; + + // Default is a special case (and would result in invalid syntax) and kinda fucked up: https://github.com/evanw/esbuild/issues/1719#issuecomment-953470495 + if (node.left.property.name !== 'default') { + exports.add(node.left.property.name); + } + } + }); + + const imports = requires.map((id, i) => `import __repl_${i} from '${id}';`).join('\n'); + const lookup = `const __repl_lookup = { ${requires + .map((id, i) => `'${id}': __repl_${i}`) + .join(', ')} };`; + + // Special case https://github.com/mathiasbynens/CSS.escape/issues/12 + if (id.includes('css.escape')) { + code = code.replace("typeof global != 'undefined' ? global : this", 'globalThis'); + } + + const transformed = [ + imports, + lookup, + require, + `const exports = {}; const module = { exports };`, + code, + `export default module.exports;`, + ...Array.from(exports).map((name) => { + const alias = `$$module_exports_${name}`; + return `const ${alias} = module.exports.${name}; export { ${alias} as ${name} };`; + }) + ].join('\n\n'); + + return { + code: transformed, + map: null + }; + } catch (err) { + return null; + } + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/bundler/plugins/csv.ts b/packages/repl/src/lib/workers/bundler/plugins/csv.ts new file mode 100644 index 000000000..baa7879b5 --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/csv.ts @@ -0,0 +1,15 @@ +import type { Plugin } from '@rollup/browser'; + +const plugin: Plugin = { + name: 'csv', + transform: (code, id) => { + if (!id.endsWith('.csv')) return; + + return { + code: `import { csvParse, autoType } from 'd3-dsv';\nexport default csvParse(\`${code}\`, autoType);`, + map: null + }; + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/bundler/plugins/glsl.ts b/packages/repl/src/lib/workers/bundler/plugins/glsl.ts new file mode 100644 index 000000000..e0af03c11 --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/glsl.ts @@ -0,0 +1,15 @@ +import type { Plugin } from '@rollup/browser'; + +const plugin: Plugin = { + name: 'glsl', + transform: (code, id) => { + if (!id.endsWith('.glsl')) return; + + return { + code: `export default ${JSON.stringify(code)};`, + map: null + }; + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/bundler/plugins/image.ts b/packages/repl/src/lib/workers/bundler/plugins/image.ts new file mode 100644 index 000000000..cbcbaa72d --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/image.ts @@ -0,0 +1,16 @@ +import type { Plugin } from '@rollup/browser'; + +const plugin: Plugin = { + name: 'image', + transform: (code, id) => { + const match = id.match(/\.(png|webp)$/); + if (!match) return; + + return { + code: `export default "data:image/${match[1]};base64,${code}";`, + map: null + }; + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/bundler/plugins/json.ts b/packages/repl/src/lib/workers/bundler/plugins/json.ts new file mode 100644 index 000000000..cd09a5021 --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/json.ts @@ -0,0 +1,15 @@ +import type { Plugin } from '@rollup/browser'; + +const plugin: Plugin = { + name: 'json', + transform: (code, id) => { + if (!id.endsWith('.json')) return; + + return { + code: `export default ${code};`, + map: null + }; + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/bundler/plugins/loop-protect.ts b/packages/repl/src/lib/workers/bundler/plugins/loop-protect.ts new file mode 100644 index 000000000..1bdc6e37e --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/loop-protect.ts @@ -0,0 +1,113 @@ +import type { Plugin } from '@rollup/browser'; +import { parse } from 'acorn'; +import { print } from 'esrap'; +import ts from 'esrap/languages/ts'; +import type { + ArrowFunctionExpression, + BlockStatement, + DoWhileStatement, + ForStatement, + FunctionDeclaration, + FunctionExpression, + Node, + Statement, + WhileStatement +} from 'estree'; +import { walk, type Context } from 'zimmerframe'; + +const TIMEOUT = 100; + +const regex = /\b(for|while)\b/; + +function parse_statement(code: string): Statement { + return parse(code, { ecmaVersion: 'latest' }).body[0] as Statement; +} + +const declaration = parse_statement(` + const __start = Date.now(); +`); + +const check = parse_statement(` + if (Date.now() > __start + ${TIMEOUT}) { + throw new Error('Infinite loop detected'); + } +`); + +export function get_current_function( + path: Node[] +): null | FunctionExpression | FunctionDeclaration | ArrowFunctionExpression { + for (let i = path.length - 1; i >= 0; i--) { + const node = path[i]; + if ( + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' || + node.type === 'ArrowFunctionExpression' + ) { + return node; + } + } + return null; +} + +function loop_protect( + node: Statement, + context: Context +): Node | void { + const current_function = get_current_function(context.path); + + if (current_function === null || (!current_function.async && !current_function.generator)) { + const body = context.visit(node.body) as import('estree').Statement; + + const statements = body.type === 'BlockStatement' ? [...body.body] : [body]; + + const replacement: BlockStatement = { + type: 'BlockStatement', + body: [ + declaration, + { + ...((context.next() ?? node) as Statement), + body: { + type: 'BlockStatement', + body: [...statements, check] + } + } + ] + }; + + return replacement; + } + + context.next(); +} + +const plugin: Plugin = { + name: 'loop-protect', + transform: (code, id) => { + // only applies to local files, not imports + if (!id.startsWith('./')) return; + + // only applies to JS and Svelte files + if (!id.endsWith('.js') && !id.endsWith('.svelte')) return; + + // fast path + if (!regex.test(code)) return; + + const ast = parse(code, { + ecmaVersion: 'latest', + sourceType: 'module' + }); + + const transformed = walk(ast as Node, null, { + WhileStatement: loop_protect, + DoWhileStatement: loop_protect, + ForStatement: loop_protect + }); + + // nothing changed + if (ast === transformed) return null; + + return print(transformed, ts()); + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/bundler/plugins/mp3.ts b/packages/repl/src/lib/workers/bundler/plugins/mp3.ts new file mode 100644 index 000000000..4271fd0a8 --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/mp3.ts @@ -0,0 +1,15 @@ +import type { Plugin } from '@rollup/browser'; + +const plugin: Plugin = { + name: 'mp3', + transform: (code, id) => { + if (!id.endsWith('.mp3')) return; + + return { + code: `export default "data:audio/mp3;base64,${code}";`, + map: null + }; + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/bundler/plugins/replace.ts b/packages/repl/src/lib/workers/bundler/plugins/replace.ts new file mode 100644 index 000000000..3908997ff --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/replace.ts @@ -0,0 +1,60 @@ +import type { Plugin } from '@rollup/browser'; + +function escape(str: string) { + return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); +} + +function ensureFunction(functionOrValue: unknown) { + if (typeof functionOrValue === 'function') { + return functionOrValue; + } + return function () { + return functionOrValue; + }; +} + +function longest(a: string, b: string) { + return b.length - a.length; +} + +function mapToFunctions(object: Record) { + return Object.keys(object).reduce(function (functions: Record, key) { + functions[key] = ensureFunction(object[key]); + return functions; + }, {}); +} + +function replace(options: Record): Plugin { + const functionValues = mapToFunctions(options); + const keys = Object.keys(functionValues).sort(longest).map(escape); + + const pattern = new RegExp('\\b(' + keys.join('|') + ')\\b', 'g'); + + return { + name: 'replace', + + transform: function transform(code, id) { + let hasReplacements = false; + let match; + let start; + let end; + let replacement; + + code = code.replace(pattern, (_, key) => { + hasReplacements = true; + return String(functionValues[key](id)); + }); + + if (!hasReplacements) { + return null; + } + + return { + code, + map: null + }; + } + }; +} + +export default replace; diff --git a/packages/repl/src/lib/workers/bundler/plugins/svg.ts b/packages/repl/src/lib/workers/bundler/plugins/svg.ts new file mode 100644 index 000000000..05b6179b6 --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/svg.ts @@ -0,0 +1,15 @@ +import type { Plugin } from '@rollup/browser'; + +const plugin: Plugin = { + name: 'svg', + transform: (code, id) => { + if (!id.endsWith('.svg')) return; + + return { + code: `export default ${btoa(code)};`, + map: null + }; + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/bundler/plugins/typescript.ts b/packages/repl/src/lib/workers/bundler/plugins/typescript.ts new file mode 100644 index 000000000..de520c948 --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/plugins/typescript.ts @@ -0,0 +1,16 @@ +import { strip_types } from '../../typescript-strip-types'; +import type { Plugin } from '@rollup/browser'; + +const plugin: Plugin = { + name: 'typescript-strip-types', + transform: (code, id) => { + const match = id.endsWith('.ts'); + if (!match) return; + + return { + code: strip_types(code) + }; + } +}; + +export default plugin; diff --git a/packages/repl/src/lib/workers/bundler/semver.ts b/packages/repl/src/lib/workers/bundler/semver.ts new file mode 100644 index 000000000..17d57548a --- /dev/null +++ b/packages/repl/src/lib/workers/bundler/semver.ts @@ -0,0 +1,49 @@ +// https://devhints.io/semver +export function max(version: string) { + if (!version || version === '*' || version === 'x') { + return 'latest'; + } + + // strip any * parts, e.g. 1.2.x becomes 1.2 + version = version.replace(/\.[x*].+/, ''); + + const match = /^([~^])?(\d+|[*x])(?:\.(\d+|[*x])(?:\.(\d+|[*x]))?)?(-.*)?$/.exec(version); + + if (!match) { + // bail + console.warn(`Could not resolve version from ${version}`); + return 'latest'; + } + + const [_, qualifier, major, minor, _patch, prerelease] = match; + + // prerelease versions (e.g. ^2.0.0-next.18) should be returned as-is + // since the stable version they'd resolve to may not exist yet + if (prerelease) { + return qualifier ? version.slice(qualifier.length) : version; + } + + // ^ means 'same major', unless 0.x + if (qualifier === '^') { + if (major === '0') { + if (minor === '0') { + return version.slice(1); + } + + return `${major}.${minor}`; + } + + return major; + } + + // ~ means 'same minor' + if (qualifier === '~') { + if (minor !== undefined) { + return `${major}.${minor}`; + } + + return major; + } + + return version; +} diff --git a/packages/repl/src/lib/workers/compiler/index.ts b/packages/repl/src/lib/workers/compiler/index.ts new file mode 100644 index 000000000..8557fc1dd --- /dev/null +++ b/packages/repl/src/lib/workers/compiler/index.ts @@ -0,0 +1,135 @@ +import '@sveltejs/site-kit/polyfills'; +import type { CompileResult } from 'svelte/compiler'; +import type { ExposedCompilerOptions, File } from '../../Workspace.svelte'; +import { load_svelte } from '../npm'; +import { strip_types } from '../typescript-strip-types'; + +// hack for magic-string and Svelte 4 compiler +// do not put this into a separate module and import it, would be treeshaken in prod +self.window = self; + +declare var self: Window & typeof globalThis & { svelte: typeof import('svelte/compiler') }; + +const cache: Record> = {}; + +addEventListener('message', async (event) => { + const { id, file, version, options } = event.data as { + id: number; + file: File; + version: string; + options: ExposedCompilerOptions; + }; + + cache[version] ??= load_svelte(version); + cache[version].catch(() => { + delete cache[version]; + }); + const { can_use_experimental_async, svelte } = await cache[version]; + + if (!file.name.endsWith('.svelte') && !svelte.compileModule) { + // .svelte.js file compiled with Svelte 3/4 compiler + postMessage({ + id, + filename: file.name, + payload: { + error: null, + result: null, + migration: null + } + }); + return; + } + + let migration = null; + + if (svelte.migrate) { + try { + migration = svelte.migrate(file.contents, { filename: file.name }); + } catch (e) { + // can this happen? + } + } + + try { + let result: CompileResult; + + if (file.name.endsWith('.svelte')) { + const is_svelte_3_or_4 = !svelte.compileModule; + const compilerOptions: any = { + generate: is_svelte_3_or_4 + ? options.generate === 'client' + ? 'dom' + : 'ssr' + : options.generate, + dev: options.dev, + filename: file.name, + fragments: options.fragments + }; + + if (!is_svelte_3_or_4) { + compilerOptions.modernAst = options.modernAst; // else Svelte 3/4 will throw an "unknown option" error + } + + if (can_use_experimental_async) { + compilerOptions.experimental = { async: true }; + } + + if (compilerOptions.fragments == null) { + // if fragments is not set it probably means we are using + // a version that doesn't support it, so we need to remove it + delete compilerOptions.fragments; + } + + result = svelte.compile(file.contents, compilerOptions); + } else { + const compilerOptions: any = { + generate: options.generate, + dev: options.dev, + filename: file.name + }; + + if (can_use_experimental_async) { + compilerOptions.experimental = { async: true }; + } + + const content = file.basename.endsWith('.ts') + ? strip_types(file.contents) + : file.contents; + result = svelte.compileModule(content, compilerOptions); + } + + postMessage({ + id, + filename: file.name, + payload: { + error: null, + result: { + metadata: { runes: false }, + ...result, + warnings: result.warnings.map((w) => { + delete w.toString; + return { message: w.message, ...w }; + }) + }, + migration + } + }); + } catch (e) { + if (!e.position && e.loc) { + // this came from tsBlankSpace. Workspace expects a + // `position` property from a Svelte compile error; + // this is a hacky but pragmatic way to solve it + e.position = [e.pos, e.raisedAt]; + } + + postMessage({ + id, + filename: file.name, + payload: { + error: { message: e.message, ...e }, + result: null, + migration: null + } + }); + } +}); diff --git a/packages/repl/src/lib/workers/constants.ts b/packages/repl/src/lib/workers/constants.ts new file mode 100644 index 000000000..dd2fad619 --- /dev/null +++ b/packages/repl/src/lib/workers/constants.ts @@ -0,0 +1,2 @@ +export const VIRTUAL = 'virtual://$'; +export const NPM = 'npm://$'; diff --git a/packages/repl/src/lib/workers/npm.ts b/packages/repl/src/lib/workers/npm.ts new file mode 100644 index 000000000..c9416addc --- /dev/null +++ b/packages/repl/src/lib/workers/npm.ts @@ -0,0 +1,251 @@ +import * as resolve from 'resolve.exports'; +import { parseTar, type FileDescription } from 'tarparser'; +import { NPM } from './constants'; + +export interface Package { + meta: any; // package.json contents + contents: Record; +} + +/** map of `pkg-name@1` -> `1.2.3` */ +const versions = new Map>(); +const packages = new Map>(); + +const pkg_pr_new_regex = /^(pr|commit|branch)-(.+)/; + +export async function load_svelte(version: string) { + if (version === 'local') { + await import(/* @vite-ignore */ `${location.origin}/svelte/compiler/index.js`); + } else { + if (!pkg_pr_new_regex.test(version)) { + const resolved_version = await resolve_version('svelte', version); + if (resolved_version) { + version = resolved_version; + } else { + throw new Error(`Failed to resolve svelte@${version}`); + } + } + + const pkg = await fetch_package('svelte', version); + + const entry = version.startsWith('3.') + ? 'compiler.js' + : version.startsWith('4.') + ? 'compiler.cjs' + : 'compiler/index.js'; + + const compiler = pkg.contents[entry].text; + + (0, eval)(compiler + `\n//# sourceURL=${entry}@` + version); + } + + console.log(`Using Svelte compiler version ${version}`); + + let can_use_experimental_async = false; + + try { + svelte.compileModule('', { + generate: 'client', + experimental: { + async: true + } + }); + + can_use_experimental_async = true; + } catch (e) { + // do nothing + } + + return { + svelte, + version, + can_use_experimental_async + }; +} + +export async function resolve_version(name: string, version: string): Promise { + if (pkg_pr_new_regex.test(version)) { + return version; + } + + const key = `${name}@${version}`; + + if (!versions.has(key)) { + const promise = fetch( + `https://data.jsdelivr.com/v1/packages/npm/${name}/resolved?specifier=${version}` + ).then(async (r) => { + if (!r.ok) { + versions.delete(key); + throw new Error(`Failed to import ${key}. Are you sure the package exists?`); + } + + return (await r.json()).version; + }); + + versions.set(key, promise); + } + + return await versions.get(key)!; +} + +export async function fetch_package(name: string, version: string): Promise { + const key = `${name}@${version}`; + + if (!packages.has(key)) { + const match = pkg_pr_new_regex.exec(version); + + const url = match + ? `https://pkg.pr.new/svelte@${match[2]}` + : `https://registry.npmjs.org/${name}/-/${name.split('/').pop()}-${version}.tgz`; + + const promise = fetch(url).then(async (r) => { + if (!r.ok) { + packages.delete(url); + throw new Error(`Failed to fetch ${url}`); + } + + const contents: Record = {}; + + for (const file of await parseTar(await r.arrayBuffer())) { + if (file.type === 'file') { + contents[file.name.slice(8)] = file; // remove `package/` prefix + } + } + + const meta = JSON.parse(contents['package.json'].text); + + return { meta, contents }; + }); + + packages.set(key, promise); + } + + return packages.get(key)!; +} + +export function resolve_subpath(pkg: Package, subpath: string): string { + // match legacy Rollup logic — pkg.svelte takes priority over pkg.exports + if (typeof pkg.meta.svelte === 'string' && subpath === '.') { + return `./${pkg.meta.svelte.replace('./', '')}`; + } + + if (subpath[0] === '#') { + try { + const resolved = resolve.imports(pkg.meta, subpath, { + browser: true, + conditions: ['svelte', 'module', 'browser', 'development'] + }); + + return resolved?.[0] as string; + } catch { + throw new Error( + `No matched import path was found for "${subpath}" in "${pkg.meta.name}/package.json"` + ); + } + } + + // modern + if (pkg.meta.exports) { + try { + const resolved = resolve.exports(pkg.meta, subpath, { + browser: true, + conditions: ['svelte', 'module', 'browser', 'development'] + }); + + return resolved?.[0] as string; + } catch { + throw new Error( + `No matched export path was found for "${subpath}" in "${pkg.meta.name}/package.json"` + ); + } + } + + // legacy + if (subpath === '.') { + let resolved_id = resolve.legacy(pkg.meta, { + fields: ['browser', 'module', 'main'] + }); + + if (typeof resolved_id === 'object' && !Array.isArray(resolved_id)) { + const subpath = resolved_id['.']; + if (subpath === false) return 'data:text/javascript,export {}'; + + resolved_id = + subpath ?? + resolve.legacy(pkg.meta, { + fields: ['module', 'main'] + }); + } + + if (!resolved_id) { + // last ditch — try to match index.js/index.mjs + if (pkg.contents['index.mjs']) return './index.mjs'; + if (pkg.contents['index.js']) return './index.js'; + + throw new Error(`Could not find entry point in "${pkg.meta.name}/package.json"`); + } + + return resolved_id as string; + } + + if (typeof pkg.meta.browser === 'object') { + // this will either return `pkg.browser[subpath]` or `subpath` + return resolve.legacy(pkg.meta, { + browser: subpath + }) as string; + } + + return subpath; +} + +export function normalize_path(pkg: Package, path: string, importee: string, importer: string) { + for (const suffix of ['', '.js', '.mjs', '.cjs', '/index.js', '/index.mjs', '/index.cjs']) { + let with_suffix = path + suffix; + + if (pkg.meta.browser) { + with_suffix = pkg.meta.browser[`./${with_suffix}`]?.replace('./', '') ?? with_suffix; + } + + const file = pkg.contents[with_suffix]; + + if (file && file.type === 'file') { + return `${NPM}/${pkg.meta.name}@${pkg.meta.version}/${with_suffix}`; + } + } + + throw new Error( + `Could not find ${path} in ${pkg.meta.name}@${pkg.meta.version} (error occurred while trying to resolve ${importee} within ${importer})` + ); +} + +const LOCAL_PKG_URL = `${location.origin}/svelte/package.json`; +let local_svelte_pkg: Promise; + +export async function resolve_local(specifier: string) { + const pkg = await (local_svelte_pkg ??= fetch(LOCAL_PKG_URL).then((r) => r.json())); + + const subpath = + specifier[0] === '#' + ? (resolve.imports(pkg, specifier, { + browser: true + })![0] as string) + : (resolve.exports(pkg, specifier.replace('svelte', '.'), { + browser: true + })![0] as string); + + return new URL(subpath, LOCAL_PKG_URL).href; +} + +export function parse_npm_url(href: string) { + const match = /^npm:\/\/\$\/((?:@[^/]+\/)?[^/@]+)(?:@([^/]+))?(\/.+)?$/.exec(href); + + if (!match) { + throw new Error(`${href} is not a valid npm URL`); + } + + return { + name: match[1], + version: match[2], + subpath: match[3] + }; +} diff --git a/packages/repl/src/lib/workers/patch_window.ts b/packages/repl/src/lib/workers/patch_window.ts new file mode 100644 index 000000000..ff7057c9c --- /dev/null +++ b/packages/repl/src/lib/workers/patch_window.ts @@ -0,0 +1 @@ +self.window = self; // hack for magic-sring and rollup inline sourcemaps diff --git a/packages/repl/src/lib/workers/typescript-strip-types.ts b/packages/repl/src/lib/workers/typescript-strip-types.ts new file mode 100644 index 000000000..3e66b5bc9 --- /dev/null +++ b/packages/repl/src/lib/workers/typescript-strip-types.ts @@ -0,0 +1,308 @@ +import * as acorn from 'acorn'; +import { walk, type Context, type Visitors } from 'zimmerframe'; +import { tsPlugin } from '@sveltejs/acorn-typescript'; +import MagicString from 'magic-string'; + +const ParserWithTS = acorn.Parser.extend(tsPlugin()); + +/** + * @param {FunctionExpression | FunctionDeclaration} node + * @param {Context} context + */ +function remove_this_param_and_optional( + node: acorn.FunctionExpression | acorn.FunctionDeclaration, + context: Context +) { + for (const param of node.params as Array>) { + if (param?.type === 'Identifier') { + if (param.name === 'this') { + if (param.typeAnnotation) { + // the type annotation is blanked by another visitor, do it in two parts to prevent an overwrite error + ts_blank_space(context, { + start: param.start, + end: param.typeAnnotation.start + }); + ts_blank_space(context, { + start: param.typeAnnotation.end, + end: node.params[1]?.start || param.end + }); + } else { + ts_blank_space(context, { + start: param.start, + end: node.params[1]?.start || param.end + }); + } + } else if (param.optional) { + const question_start = context.state.ms.original.indexOf('?', param.start); + if (question_start !== -1 && question_start < param.end) { + ts_blank_space(context, { start: question_start, end: question_start + 1 }); + } + } + } + } + return context.next(); +} + +function typescript_invalid_feature(node: any, feature: string) { + const e = new Error(`The REPL does not support ${feature}. Please remove it from your code.`); + // @ts-expect-error Our REPL error handling needs this + e.position = [node.start, node.end]; + throw e; +} + +const empty = { + type: 'EmptyStatement' +}; + +function ts_blank_space(context: Context, node: any): void { + const { start, end } = node; + let i = start; + while (i < end) { + // Skip whitespace + while (i < end && /\s/.test(context.state.ms.original[i])) i++; + if (i >= end) break; + // Find next whitespace or end + let j = i + 1; + while (j < end && !/\s/.test(context.state.ms.original[j])) j++; + context.state.ms.overwrite(i, j, ' '.repeat(j - i)); + i = j; + } +} + +function specifier_end(node: any, i: number, s: any, context: Context) { + let end = node.specifiers[i + 1]?.start; + if (end === undefined) { + end = s.end; + // Look for a comma after s.end, skipping whitespace + let j = end; + const original = context.state.ms.original; + while (j < original.length && /\s/.test(original[j])) j++; + if (original[j] === ',') { + end = j + 1; + } + } + return end; +} + +const visitors: Visitors = { + _(node, context) { + if (node.typeAnnotation) ts_blank_space(context, node.typeAnnotation); + if (node.typeParameters) ts_blank_space(context, node.typeParameters); + if (node.typeArguments) ts_blank_space(context, node.typeArguments); + if (node.returnType) ts_blank_space(context, node.returnType); + if (node.accessibility) { + ts_blank_space(context, { + start: node.start, + end: node.start + node.accessibility.length + }); + } + + delete node.typeAnnotation; + delete node.typeParameters; + delete node.typeArguments; + delete node.returnType; + delete node.accessibility; + + context.next(); + }, + Decorator(node, context) { + ts_blank_space(context, node); + }, + ImportDeclaration(node, context) { + if (node.importKind === 'type') { + ts_blank_space(context, node); + return empty; + } + + if (node.specifiers?.length > 0) { + const specifiers = node.specifiers.filter((s: any, i: number) => { + if (s.importKind !== 'type') return true; + + ts_blank_space(context, { + start: s.start, + end: specifier_end(node, i, s, context) + }); + }); + + if (specifiers.length === 0) { + ts_blank_space(context, node); + } + } + }, + ExportNamedDeclaration(node, context) { + if (node.exportKind === 'type') { + ts_blank_space(context, node); + return empty; + } + + if (node.declaration) { + const result = context.next(); + if (result?.declaration?.type === 'EmptyStatement') { + ts_blank_space(context, node); + return empty; + } + return result; + } + + if (node.specifiers) { + const specifiers = node.specifiers.filter((s: any, i: number) => { + if (s.exportKind !== 'type') return true; + + ts_blank_space(context, { + start: s.start, + end: specifier_end(node, i, s, context) + }); + }); + + if (specifiers.length === 0) { + ts_blank_space(context, node); + } + return; + } + }, + ExportDefaultDeclaration(node, context) { + if (node.exportKind === 'type') { + ts_blank_space(context, node); + return empty; + } else { + context.next(); + } + }, + ExportAllDeclaration(node, context) { + if (node.exportKind === 'type') { + ts_blank_space(context, node); + return empty; + } else { + context.next(); + } + }, + PropertyDefinition(node, context) { + if (node.accessor) { + typescript_invalid_feature( + node, + 'accessor fields (related TSC proposal is not stage 4 yet)' + ); + } else { + context.next(); + } + }, + TSAsExpression(node, context) { + ts_blank_space(context, { start: node.expression.end, end: node.end }); + context.visit(node.expression); + }, + TSSatisfiesExpression(node, context) { + ts_blank_space(context, { start: node.expression.end, end: node.end }); + context.visit(node.expression); + }, + TSNonNullExpression(node, context) { + ts_blank_space(context, { start: node.expression.end, end: node.end }); + context.visit(node.expression); + }, + TSInterfaceDeclaration(node, context) { + ts_blank_space(context, node); + return empty; + }, + TSTypeAliasDeclaration(node, context) { + ts_blank_space(context, node); + return empty; + }, + TSTypeAssertion(node, context) { + ts_blank_space(context, { start: node.start, end: node.expression.start }); + context.visit(node.expression); + }, + TSEnumDeclaration(node, context) { + typescript_invalid_feature(node, 'enums'); + }, + TSParameterProperty(node, context) { + if ((node.readonly || node.accessibility) && context.path.at(-2)?.kind === 'constructor') { + typescript_invalid_feature(node, 'accessibility modifiers on constructor parameters'); + } + ts_blank_space(context, { start: node.start, end: node.parameter.start }); + context.visit(node.parameter); + }, + TSInstantiationExpression(node, context) { + ts_blank_space(context, { start: node.start, end: node.expression.start }); + context.visit(node.expression); + }, + FunctionExpression: remove_this_param_and_optional, + FunctionDeclaration: remove_this_param_and_optional, + TSDeclareFunction(node, context) { + ts_blank_space(context, node); + return empty; + }, + ClassDeclaration(node, context) { + if (node.declare || node.abstract) { + ts_blank_space(context, node); + return empty; + } + + if (node.implements?.length) { + const implements_keyword_start = context.state.ms.original.lastIndexOf( + 'implements', + node.implements[0].start + ); + ts_blank_space(context, { + start: implements_keyword_start, + end: node.implements[node.implements.length - 1].end + }); + } + context.next(); + }, + MethodDefinition(node, context) { + if (node.abstract) { + ts_blank_space(context, { start: node.start, end: node.start + 'abstract'.length }); + return empty; + } + context.next(); + }, + VariableDeclaration(node, context) { + if (node.declare) { + ts_blank_space(context, node); + return empty; + } + context.next(); + }, + VariableDeclarator(node, context) { + if (node.definite && node.id.type === 'Identifier') { + const definite_start = context.state.ms.original.indexOf( + '!', + node.id.start + node.id.name.length + ); + ts_blank_space(context, { start: definite_start, end: definite_start + 1 }); + } + context.next(); + }, + TSModuleDeclaration(node, context) { + if (!node.body) { + ts_blank_space(context, node); + return; + } + // namespaces can contain non-type nodes + const cleaned = node.body.body.map((entry) => context.visit(entry)); + if (cleaned.some((entry) => entry !== empty)) { + typescript_invalid_feature(node, 'namespaces with non-type nodes'); + } + ts_blank_space(context, node); + } +}; + +/** + * Strips type-only constructs from TypeScript code and replaces them with blank spaces. + * Errors on non-type constructs that are not supported in the REPL. + * + * This implementation closely follows the logic of https://github.com/sveltejs/svelte/blob/main/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js + * + * Used instead of`ts-blank-space` because the latter means we need to bundle all of TypeScript, which increases the worker bundles by 9x. + */ +export function strip_types(code: string): string { + const ms = new MagicString(code); + const ast = ParserWithTS.parse(code, { + sourceType: 'module', + ecmaVersion: 16, + locations: true + }); + + walk(ast, { ms }, visitors); + + return ms.toString(); +} diff --git a/packages/repl/src/lib/workers/workers.d.ts b/packages/repl/src/lib/workers/workers.d.ts new file mode 100644 index 000000000..bde5d5929 --- /dev/null +++ b/packages/repl/src/lib/workers/workers.d.ts @@ -0,0 +1,72 @@ +import type { CompileError, CompileOptions, CompileResult, Warning } from 'svelte/compiler'; +import type { File } from '../Workspace.svelte'; +import type { MessageDetails } from '$lib/types'; + +export type CompilerCommand = + | { + id: number; + type: 'init'; + svelte_url: string; + } + | { + id: number; + type: 'compile'; + payload: CompilerInput; + } + | { + id: number; + type: 'migrate'; + payload: MigrateInput; + }; + +export interface CompilerInput { + source: string; + options: CompileOptions; + is_entry: boolean; + return_ast: boolean; + svelte_url?: string; +} + +export interface CompilerOutput { + js: string; + css: string; + ast?: CompileResult['ast']; + error?: CompileError; + warnings: Warning[]; + metadata?: { + runes: boolean; + }; +} + +export interface MigrateInput { + source: string; +} + +export interface MigrateOutput { + result: { + code: string; + }; + error?: string; +} + +export interface BundleOptions { + svelte_version: string; + tailwind?: boolean; + runes?: boolean; + fragments?: 'html' | 'tree'; + async?: boolean; + aliases?: Record; +} + +export type BundleMessageData = { + uid: number; + type: 'init' | 'bundle' | 'status' | 'error' | 'version'; + message: string; + svelte_version: string; + files: File[]; + options: BundleOptions; +}; + +declare global { + var svelte: typeof import('svelte/compiler'); +} diff --git a/packages/repl/src/routes/+layout.server.ts b/packages/repl/src/routes/+layout.server.ts new file mode 100644 index 000000000..a3d15781a --- /dev/null +++ b/packages/repl/src/routes/+layout.server.ts @@ -0,0 +1 @@ +export const ssr = false; diff --git a/packages/repl/src/routes/+page.svelte b/packages/repl/src/routes/+page.svelte new file mode 100644 index 000000000..a509b9e38 --- /dev/null +++ b/packages/repl/src/routes/+page.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/packages/repl/src/routes/v0.svelte b/packages/repl/src/routes/v0.svelte new file mode 100644 index 000000000..8ef5aa659 --- /dev/null +++ b/packages/repl/src/routes/v0.svelte @@ -0,0 +1,34 @@ + + + +
    + { + // @ts-expect-error so that v0 can react to REPL errors + window.__svelte_repl_onerror?.(error); + }} /> +
    + + diff --git a/packages/repl/svelte.config.js b/packages/repl/svelte.config.js new file mode 100644 index 000000000..07e6056c2 --- /dev/null +++ b/packages/repl/svelte.config.js @@ -0,0 +1,5 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + preprocess: [vitePreprocess()] +}; diff --git a/packages/svelteplot/scripts/fix-js-imports.js b/packages/svelteplot/scripts/fix-js-imports.js index 7c7d7f20f..aa7e08768 100644 --- a/packages/svelteplot/scripts/fix-js-imports.js +++ b/packages/svelteplot/scripts/fix-js-imports.js @@ -161,7 +161,11 @@ export async function fullySpecifyImportsInDirectory(rootDir = 'dist') { const replacements = []; for (const match of matches) { - const replacement = await resolveRelativeSpecifierOnDisk(match.specifier, filePath, absoluteRoot); + const replacement = await resolveRelativeSpecifierOnDisk( + match.specifier, + filePath, + absoluteRoot + ); if (replacement && replacement !== match.specifier) { replacements.push({ start: match.start, diff --git a/packages/svelteplot/src/Mark.svelte b/packages/svelteplot/src/Mark.svelte index 43060d2be..cc2acb63f 100644 --- a/packages/svelteplot/src/Mark.svelte +++ b/packages/svelteplot/src/Mark.svelte @@ -98,18 +98,21 @@ // let mark2 = $state(mark); const facetMode = $derived(options.facet || 'auto'); - const optionsWithAutoFacet = $derived({ - ...options, - __firstFacet: left && top, - ...(facet && + const globalFacetChannels = $derived( + facet && facet.data && ((facetMode === 'auto' && facet.data === data) || facetMode === 'include') - ? { - fx: facet.x, - fy: facet.y - } - : {}) - }); + ? { fx: facet.x, fy: facet.y } + : {} + ); + + // Options merged with global facet channels, without __firstFacet. + // Used for data filtering and channel resolution so that global-facet + // marks are correctly filtered per panel and have fx/fy values resolved. + const optionsWithFacet = $derived({ ...options, ...globalFacetChannels }); + + // Same but includes __firstFacet for mark registration / getEmptyFacets. + const optionsWithAutoFacet = $derived({ ...optionsWithFacet, __firstFacet: left && top }); let added = false; @@ -145,7 +148,7 @@ data .map((d, i) => ({ ...d, [INDEX]: i })) .flatMap((row, index) => { - const channels = options as Record>; + const channels = optionsWithFacet as Record>; if (!testFacet(row, channels) || !testFilter(row, channels)) return []; const out: ResolvedDataRecord = { datum: row, @@ -156,10 +159,10 @@ ScaleName ][]) { // check if the mark has defined an accessor for this channel - if ((options as any)?.[channel] !== undefined && out[channel] === undefined) { + if ((optionsWithFacet as any)?.[channel] !== undefined && out[channel] === undefined) { // resolve value - out[channel] = resolveChannel(channel, row, options as any); - if ((options as any)[channel] === INDEX) { + out[channel] = resolveChannel(channel, row, optionsWithFacet as any); + if ((optionsWithFacet as any)[channel] === INDEX) { const scale = plot.scales[CHANNEL_SCALE[channel]]; if (scale.type === 'band' || scale.type === 'point') { out[channel] = scale.domain[out[channel] % scale.domain.length]; @@ -280,7 +283,7 @@ ][]) { // check if the mark has defined an accessor for this channel if ( - (options as any)?.[channel] != null && + (optionsWithFacet as any)?.[channel] != null && (out as any)[channel] === undefined ) { // resolve value diff --git a/packages/svelteplot/src/Plot.svelte b/packages/svelteplot/src/Plot.svelte index 08091f4cd..0635feb41 100644 --- a/packages/svelteplot/src/Plot.svelte +++ b/packages/svelteplot/src/Plot.svelte @@ -3,7 +3,7 @@ The Plot component is the container for plots. It collects the marks with their data and channels and computes the shared scales. - The Plot component is split into two parts. This is the outer Plot which + The Plot component is split into two parts. This is the outer Plot which provides convenient defaults and automatically adds axes etc to the graphics. The downside is that it adds a bunch of imports that you may not be using. To help with this you can use the core/Plot component directly for a more @@ -97,13 +97,14 @@ {#if userFooter}{@render userFooter?.()}{/if} {/snippet} - = { }; export const CSS_VAR = /^var\(--([a-z-0-9,\s]+)\)$/; -export const CSS_COLOR = /^color\(/; +export const CSS_COLOR = /^(?:color|light-dark)\(/; export const CSS_COLOR_MIX = /^color-mix\(/; // just check for prefix export const CSS_COLOR_CONTRAST = /^color-contrast\(/; // just check for prefix export const CSS_RGBA = /^rgba\(/; // just check for prefix diff --git a/packages/svelteplot/src/core/Plot.svelte b/packages/svelteplot/src/core/Plot.svelte index 20f9cb8e6..7c82e1e69 100644 --- a/packages/svelteplot/src/core/Plot.svelte +++ b/packages/svelteplot/src/core/Plot.svelte @@ -5,7 +5,7 @@ you want to reduce the footprint of the plot to the bare minimum. Keep in mind that you will have to create your own scales if you're using - this component. + this component. --> + + diff --git a/src/content/tutorial/01-basics/01-getting-started/01-welcome/index.md b/src/content/tutorial/01-basics/01-getting-started/01-welcome/index.md new file mode 100644 index 000000000..4cee296d2 --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/01-welcome/index.md @@ -0,0 +1,13 @@ +--- +title: Welcome to SveltePlot +--- + +Welcome to the SveltePlot tutorial! Here you will learn how to use SveltePlot to create beautiful, interactive data visualizations with ease. + +You can also look at the [Examples section](/examples) or consult the documentation. + +## What is SveltePlot? + +SveltePlot is a plotting library for Svelte. It provides a simple and intuitive API for creating a wide variety of charts and visualizations, following the grammar of graphics principles. + +This means that rather than relying on templates such as a "scatterplot", in SveltePlot you construct graphics from building blocks like the Dot mark, the same dot mark that you can use for a dot plot. diff --git a/src/content/tutorial/01-basics/01-getting-started/02-first-plot/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/01-getting-started/02-first-plot/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..64bdc6755 --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/02-first-plot/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,5 @@ + + +

    Loaded {data.length} penguins.

    diff --git a/src/content/tutorial/01-basics/01-getting-started/02-first-plot/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/01-getting-started/02-first-plot/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..42982149c --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/02-first-plot/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/content/tutorial/01-basics/01-getting-started/02-first-plot/index.md b/src/content/tutorial/01-basics/01-getting-started/02-first-plot/index.md new file mode 100644 index 000000000..c6538ecfa --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/02-first-plot/index.md @@ -0,0 +1,39 @@ +--- +title: Your first plot +--- + +Let's dive right in. We have penguin measurement data loaded — 343 birds, each with a bill length and body mass. You can switch to the `penguins.csv` tab to take a look at the raw CSV data. Now let's visualize it. + +## Your first plot + +The first step is to import the `Plot` and `Dot` componetents from `svelteplot`: + +```svelte + +``` + +The [Plot](/features/plot) component is the root componenent for all SveltePlot graphics. The [Dot](/marks/dot) component is one of the many marks you can use to display data (more on marks later). + +To put Plot and Dot into work we replace the paragraph with a `` and a `` mark inside it, to create a dot plot: + +```svelte +-

    Loaded {data.length} penguins.

    ++ ++ ++ +``` + +`x="body_mass_g"` and `y="species"` tell SveltePlot which columns to map to horizontal and vertical position. Axes and tick labels appear automatically. + +This simple dot plot tells us that Gentoo penguins are heavier than Adelie and Chinstrap. + +By default, dots show up as outlines (which makes it easier to see overlapping symbols), but you can pass the `fill` property to change that: + +```svelte + + + +``` diff --git a/src/content/tutorial/01-basics/01-getting-started/03-channels/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/01-getting-started/03-channels/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..4c5e61d4d --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/03-channels/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/content/tutorial/01-basics/01-getting-started/03-channels/index.md b/src/content/tutorial/01-basics/01-getting-started/03-channels/index.md new file mode 100644 index 000000000..ea0627f7c --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/03-channels/index.md @@ -0,0 +1,38 @@ +--- +title: Channels +--- + +The props on a mark — `x`, `y`, `r`, `fill`, `opacity`, and others — are called **channels**. A channel maps a data column (e.g. body mass) to a visual property (e.g. the horizontal position). If we change the `y` channel to `bill_length_mm`, we turn the dot plot into a **scatterplot**! + +```svelte + + + +``` + +It's still the same `Dot` component, just a different channel assignment! That's the magic of the grammar of graphics. Now use the `fill` channel to color each dot by species: + +```svelte + + + +``` + +SveltePlot sees that `fill` maps to string values and assigns a categorical color scheme automatically. + +To see the meaning of each color we can pass a `color={{ legend: true }}` to the Plot: + +```svelte + +``` + +We now see that while the Gentoo penguins are heavier, the Chinstrap penguins also have long bills. diff --git a/src/content/tutorial/01-basics/01-getting-started/04-marks/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/01-getting-started/04-marks/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..db7267b6e --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/04-marks/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,19 @@ + + + + + + + diff --git a/src/content/tutorial/01-basics/01-getting-started/04-marks/index.md b/src/content/tutorial/01-basics/01-getting-started/04-marks/index.md new file mode 100644 index 000000000..f9bb3700f --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/04-marks/index.md @@ -0,0 +1,44 @@ +--- +title: Marks +--- + +`Dot` is a **mark** — a visual component that maps data to geometric shapes. SveltePlot has many marks: `Dot`, `Line`, `BarY`, `RuleY`, and more. + +For instance, we can use the [Hull](/marks/delaunay#Hull) mark to add a convex hull around each species. First we import `Hull` alongside `Dot`: + +```js +import { Plot, Dot+++, Hull+++ } from 'svelteplot'; +``` + +Then we add it and pass the same data and channels as we're passing to `Dot`: + +```svelte + ++ + + +``` + +Not all marks need data. `RuleX` draws a vertical reference line at a fixed x value. Again, just import it: + +```js +import { Plot, Dot, Hull+++, RuleX+++ } from 'svelteplot'; +``` + +Then add it before the closing `` at the end: + +```svelte ++ +
    +``` + +The order in which we put the marks inside the Plot determines how they are drawn, first the hull, then the dots and the rule on top. diff --git a/src/content/tutorial/01-basics/01-getting-started/05-colors/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/01-getting-started/05-colors/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..e2360996f --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/05-colors/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,25 @@ + + + + + + + diff --git a/src/content/tutorial/01-basics/01-getting-started/05-colors/index.md b/src/content/tutorial/01-basics/01-getting-started/05-colors/index.md new file mode 100644 index 000000000..a2aaaaefa --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/05-colors/index.md @@ -0,0 +1,21 @@ +--- +title: Customizing colors +--- + +If we want to customize the colors we can create our own color scheme: + +```js +const scheme = { + Adelie: 'hotpink', + Chinstrap: 'teal', + Gentoo: 'orange' +}; +``` + +and pass it to the `color` scale options: + +```svelte + +``` + +Since the color scheme is managed by the Plot, we only have to define it once and it is applied everywhere consistently. diff --git a/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..3ecb665b5 --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/+assets/app-a/src/lib/penguins.csv b/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/+assets/app-a/src/lib/penguins.csv new file mode 100644 index 000000000..83f32630b --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/+assets/app-a/src/lib/penguins.csv @@ -0,0 +1,345 @@ +species,island,bill_length_mm,bill_depth_mm,flipper_length_mm,body_mass_g,sex +Adelie,Torgersen,39.1,18.7,181,3750,MALE +Adelie,Torgersen,39.5,17.4,186,3800,FEMALE +Adelie,Torgersen,40.3,18,195,3250,FEMALE +Adelie,Torgersen,NaN,NaN,NaN,NaN, +Adelie,Torgersen,36.7,19.3,193,3450,FEMALE +Adelie,Torgersen,39.3,20.6,190,3650,MALE +Adelie,Torgersen,38.9,17.8,181,3625,FEMALE +Adelie,Torgersen,39.2,19.6,195,4675,MALE +Adelie,Torgersen,34.1,18.1,193,3475, +Adelie,Torgersen,42,20.2,190,4250, +Adelie,Torgersen,37.8,17.1,186,3300, +Adelie,Torgersen,37.8,17.3,180,3700, +Adelie,Torgersen,41.1,17.6,182,3200,FEMALE +Adelie,Torgersen,38.6,21.2,191,3800,MALE +Adelie,Torgersen,34.6,21.1,198,4400,MALE +Adelie,Torgersen,36.6,17.8,185,3700,FEMALE +Adelie,Torgersen,38.7,19,195,3450,FEMALE +Adelie,Torgersen,42.5,20.7,197,4500,MALE +Adelie,Torgersen,34.4,18.4,184,3325,FEMALE +Adelie,Torgersen,46,21.5,194,4200,MALE +Adelie,Biscoe,37.8,18.3,174,3400,FEMALE +Adelie,Biscoe,37.7,18.7,180,3600,MALE +Adelie,Biscoe,35.9,19.2,189,3800,FEMALE +Adelie,Biscoe,38.2,18.1,185,3950,MALE +Adelie,Biscoe,38.8,17.2,180,3800,MALE +Adelie,Biscoe,35.3,18.9,187,3800,FEMALE +Adelie,Biscoe,40.6,18.6,183,3550,MALE +Adelie,Biscoe,40.5,17.9,187,3200,FEMALE +Adelie,Biscoe,37.9,18.6,172,3150,FEMALE +Adelie,Biscoe,40.5,18.9,180,3950,MALE +Adelie,Dream,39.5,16.7,178,3250,FEMALE +Adelie,Dream,37.2,18.1,178,3900,MALE +Adelie,Dream,39.5,17.8,188,3300,FEMALE +Adelie,Dream,40.9,18.9,184,3900,MALE +Adelie,Dream,36.4,17,195,3325,FEMALE +Adelie,Dream,39.2,21.1,196,4150,MALE +Adelie,Dream,38.8,20,190,3950,MALE +Adelie,Dream,42.2,18.5,180,3550,FEMALE +Adelie,Dream,37.6,19.3,181,3300,FEMALE +Adelie,Dream,39.8,19.1,184,4650,MALE +Adelie,Dream,36.5,18,182,3150,FEMALE +Adelie,Dream,40.8,18.4,195,3900,MALE +Adelie,Dream,36,18.5,186,3100,FEMALE +Adelie,Dream,44.1,19.7,196,4400,MALE +Adelie,Dream,37,16.9,185,3000,FEMALE +Adelie,Dream,39.6,18.8,190,4600,MALE +Adelie,Dream,41.1,19,182,3425,MALE +Adelie,Dream,37.5,18.9,179,2975, +Adelie,Dream,36,17.9,190,3450,FEMALE +Adelie,Dream,42.3,21.2,191,4150,MALE +Adelie,Biscoe,39.6,17.7,186,3500,FEMALE +Adelie,Biscoe,40.1,18.9,188,4300,MALE +Adelie,Biscoe,35,17.9,190,3450,FEMALE +Adelie,Biscoe,42,19.5,200,4050,MALE +Adelie,Biscoe,34.5,18.1,187,2900,FEMALE +Adelie,Biscoe,41.4,18.6,191,3700,MALE +Adelie,Biscoe,39,17.5,186,3550,FEMALE +Adelie,Biscoe,40.6,18.8,193,3800,MALE +Adelie,Biscoe,36.5,16.6,181,2850,FEMALE +Adelie,Biscoe,37.6,19.1,194,3750,MALE +Adelie,Biscoe,35.7,16.9,185,3150,FEMALE +Adelie,Biscoe,41.3,21.1,195,4400,MALE +Adelie,Biscoe,37.6,17,185,3600,FEMALE +Adelie,Biscoe,41.1,18.2,192,4050,MALE +Adelie,Biscoe,36.4,17.1,184,2850,FEMALE +Adelie,Biscoe,41.6,18,192,3950,MALE +Adelie,Biscoe,35.5,16.2,195,3350,FEMALE +Adelie,Biscoe,41.1,19.1,188,4100,MALE +Adelie,Torgersen,35.9,16.6,190,3050,FEMALE +Adelie,Torgersen,41.8,19.4,198,4450,MALE +Adelie,Torgersen,33.5,19,190,3600,FEMALE +Adelie,Torgersen,39.7,18.4,190,3900,MALE +Adelie,Torgersen,39.6,17.2,196,3550,FEMALE +Adelie,Torgersen,45.8,18.9,197,4150,MALE +Adelie,Torgersen,35.5,17.5,190,3700,FEMALE +Adelie,Torgersen,42.8,18.5,195,4250,MALE +Adelie,Torgersen,40.9,16.8,191,3700,FEMALE +Adelie,Torgersen,37.2,19.4,184,3900,MALE +Adelie,Torgersen,36.2,16.1,187,3550,FEMALE +Adelie,Torgersen,42.1,19.1,195,4000,MALE +Adelie,Torgersen,34.6,17.2,189,3200,FEMALE +Adelie,Torgersen,42.9,17.6,196,4700,MALE +Adelie,Torgersen,36.7,18.8,187,3800,FEMALE +Adelie,Torgersen,35.1,19.4,193,4200,MALE +Adelie,Dream,37.3,17.8,191,3350,FEMALE +Adelie,Dream,41.3,20.3,194,3550,MALE +Adelie,Dream,36.3,19.5,190,3800,MALE +Adelie,Dream,36.9,18.6,189,3500,FEMALE +Adelie,Dream,38.3,19.2,189,3950,MALE +Adelie,Dream,38.9,18.8,190,3600,FEMALE +Adelie,Dream,35.7,18,202,3550,FEMALE +Adelie,Dream,41.1,18.1,205,4300,MALE +Adelie,Dream,34,17.1,185,3400,FEMALE +Adelie,Dream,39.6,18.1,186,4450,MALE +Adelie,Dream,36.2,17.3,187,3300,FEMALE +Adelie,Dream,40.8,18.9,208,4300,MALE +Adelie,Dream,38.1,18.6,190,3700,FEMALE +Adelie,Dream,40.3,18.5,196,4350,MALE +Adelie,Dream,33.1,16.1,178,2900,FEMALE +Adelie,Dream,43.2,18.5,192,4100,MALE +Adelie,Biscoe,35,17.9,192,3725,FEMALE +Adelie,Biscoe,41,20,203,4725,MALE +Adelie,Biscoe,37.7,16,183,3075,FEMALE +Adelie,Biscoe,37.8,20,190,4250,MALE +Adelie,Biscoe,37.9,18.6,193,2925,FEMALE +Adelie,Biscoe,39.7,18.9,184,3550,MALE +Adelie,Biscoe,38.6,17.2,199,3750,FEMALE +Adelie,Biscoe,38.2,20,190,3900,MALE +Adelie,Biscoe,38.1,17,181,3175,FEMALE +Adelie,Biscoe,43.2,19,197,4775,MALE +Adelie,Biscoe,38.1,16.5,198,3825,FEMALE +Adelie,Biscoe,45.6,20.3,191,4600,MALE +Adelie,Biscoe,39.7,17.7,193,3200,FEMALE +Adelie,Biscoe,42.2,19.5,197,4275,MALE +Adelie,Biscoe,39.6,20.7,191,3900,FEMALE +Adelie,Biscoe,42.7,18.3,196,4075,MALE +Adelie,Torgersen,38.6,17,188,2900,FEMALE +Adelie,Torgersen,37.3,20.5,199,3775,MALE +Adelie,Torgersen,35.7,17,189,3350,FEMALE +Adelie,Torgersen,41.1,18.6,189,3325,MALE +Adelie,Torgersen,36.2,17.2,187,3150,FEMALE +Adelie,Torgersen,37.7,19.8,198,3500,MALE +Adelie,Torgersen,40.2,17,176,3450,FEMALE +Adelie,Torgersen,41.4,18.5,202,3875,MALE +Adelie,Torgersen,35.2,15.9,186,3050,FEMALE +Adelie,Torgersen,40.6,19,199,4000,MALE +Adelie,Torgersen,38.8,17.6,191,3275,FEMALE +Adelie,Torgersen,41.5,18.3,195,4300,MALE +Adelie,Torgersen,39,17.1,191,3050,FEMALE +Adelie,Torgersen,44.1,18,210,4000,MALE +Adelie,Torgersen,38.5,17.9,190,3325,FEMALE +Adelie,Torgersen,43.1,19.2,197,3500,MALE +Adelie,Dream,36.8,18.5,193,3500,FEMALE +Adelie,Dream,37.5,18.5,199,4475,MALE +Adelie,Dream,38.1,17.6,187,3425,FEMALE +Adelie,Dream,41.1,17.5,190,3900,MALE +Adelie,Dream,35.6,17.5,191,3175,FEMALE +Adelie,Dream,40.2,20.1,200,3975,MALE +Adelie,Dream,37,16.5,185,3400,FEMALE +Adelie,Dream,39.7,17.9,193,4250,MALE +Adelie,Dream,40.2,17.1,193,3400,FEMALE +Adelie,Dream,40.6,17.2,187,3475,MALE +Adelie,Dream,32.1,15.5,188,3050,FEMALE +Adelie,Dream,40.7,17,190,3725,MALE +Adelie,Dream,37.3,16.8,192,3000,FEMALE +Adelie,Dream,39,18.7,185,3650,MALE +Adelie,Dream,39.2,18.6,190,4250,MALE +Adelie,Dream,36.6,18.4,184,3475,FEMALE +Adelie,Dream,36,17.8,195,3450,FEMALE +Adelie,Dream,37.8,18.1,193,3750,MALE +Adelie,Dream,36,17.1,187,3700,FEMALE +Adelie,Dream,41.5,18.5,201,4000,MALE +Chinstrap,Dream,46.5,17.9,192,3500,FEMALE +Chinstrap,Dream,50,19.5,196,3900,MALE +Chinstrap,Dream,51.3,19.2,193,3650,MALE +Chinstrap,Dream,45.4,18.7,188,3525,FEMALE +Chinstrap,Dream,52.7,19.8,197,3725,MALE +Chinstrap,Dream,45.2,17.8,198,3950,FEMALE +Chinstrap,Dream,46.1,18.2,178,3250,FEMALE +Chinstrap,Dream,51.3,18.2,197,3750,MALE +Chinstrap,Dream,46,18.9,195,4150,FEMALE +Chinstrap,Dream,51.3,19.9,198,3700,MALE +Chinstrap,Dream,46.6,17.8,193,3800,FEMALE +Chinstrap,Dream,51.7,20.3,194,3775,MALE +Chinstrap,Dream,47,17.3,185,3700,FEMALE +Chinstrap,Dream,52,18.1,201,4050,MALE +Chinstrap,Dream,45.9,17.1,190,3575,FEMALE +Chinstrap,Dream,50.5,19.6,201,4050,MALE +Chinstrap,Dream,50.3,20,197,3300,MALE +Chinstrap,Dream,58,17.8,181,3700,FEMALE +Chinstrap,Dream,46.4,18.6,190,3450,FEMALE +Chinstrap,Dream,49.2,18.2,195,4400,MALE +Chinstrap,Dream,42.4,17.3,181,3600,FEMALE +Chinstrap,Dream,48.5,17.5,191,3400,MALE +Chinstrap,Dream,43.2,16.6,187,2900,FEMALE +Chinstrap,Dream,50.6,19.4,193,3800,MALE +Chinstrap,Dream,46.7,17.9,195,3300,FEMALE +Chinstrap,Dream,52,19,197,4150,MALE +Chinstrap,Dream,50.5,18.4,200,3400,FEMALE +Chinstrap,Dream,49.5,19,200,3800,MALE +Chinstrap,Dream,46.4,17.8,191,3700,FEMALE +Chinstrap,Dream,52.8,20,205,4550,MALE +Chinstrap,Dream,40.9,16.6,187,3200,FEMALE +Chinstrap,Dream,54.2,20.8,201,4300,MALE +Chinstrap,Dream,42.5,16.7,187,3350,FEMALE +Chinstrap,Dream,51,18.8,203,4100,MALE +Chinstrap,Dream,49.7,18.6,195,3600,MALE +Chinstrap,Dream,47.5,16.8,199,3900,FEMALE +Chinstrap,Dream,47.6,18.3,195,3850,FEMALE +Chinstrap,Dream,52,20.7,210,4800,MALE +Chinstrap,Dream,46.9,16.6,192,2700,FEMALE +Chinstrap,Dream,53.5,19.9,205,4500,MALE +Chinstrap,Dream,49,19.5,210,3950,MALE +Chinstrap,Dream,46.2,17.5,187,3650,FEMALE +Chinstrap,Dream,50.9,19.1,196,3550,MALE +Chinstrap,Dream,45.5,17,196,3500,FEMALE +Chinstrap,Dream,50.9,17.9,196,3675,FEMALE +Chinstrap,Dream,50.8,18.5,201,4450,MALE +Chinstrap,Dream,50.1,17.9,190,3400,FEMALE +Chinstrap,Dream,49,19.6,212,4300,MALE +Chinstrap,Dream,51.5,18.7,187,3250,MALE +Chinstrap,Dream,49.8,17.3,198,3675,FEMALE +Chinstrap,Dream,48.1,16.4,199,3325,FEMALE +Chinstrap,Dream,51.4,19,201,3950,MALE +Chinstrap,Dream,45.7,17.3,193,3600,FEMALE +Chinstrap,Dream,50.7,19.7,203,4050,MALE +Chinstrap,Dream,42.5,17.3,187,3350,FEMALE +Chinstrap,Dream,52.2,18.8,197,3450,MALE +Chinstrap,Dream,45.2,16.6,191,3250,FEMALE +Chinstrap,Dream,49.3,19.9,203,4050,MALE +Chinstrap,Dream,50.2,18.8,202,3800,MALE +Chinstrap,Dream,45.6,19.4,194,3525,FEMALE +Chinstrap,Dream,51.9,19.5,206,3950,MALE +Chinstrap,Dream,46.8,16.5,189,3650,FEMALE +Chinstrap,Dream,45.7,17,195,3650,FEMALE +Chinstrap,Dream,55.8,19.8,207,4000,MALE +Chinstrap,Dream,43.5,18.1,202,3400,FEMALE +Chinstrap,Dream,49.6,18.2,193,3775,MALE +Chinstrap,Dream,50.8,19,210,4100,MALE +Chinstrap,Dream,50.2,18.7,198,3775,FEMALE +Gentoo,Biscoe,46.1,13.2,211,4500,FEMALE +Gentoo,Biscoe,50,16.3,230,5700,MALE +Gentoo,Biscoe,48.7,14.1,210,4450,FEMALE +Gentoo,Biscoe,50,15.2,218,5700,MALE +Gentoo,Biscoe,47.6,14.5,215,5400,MALE +Gentoo,Biscoe,46.5,13.5,210,4550,FEMALE +Gentoo,Biscoe,45.4,14.6,211,4800,FEMALE +Gentoo,Biscoe,46.7,15.3,219,5200,MALE +Gentoo,Biscoe,43.3,13.4,209,4400,FEMALE +Gentoo,Biscoe,46.8,15.4,215,5150,MALE +Gentoo,Biscoe,40.9,13.7,214,4650,FEMALE +Gentoo,Biscoe,49,16.1,216,5550,MALE +Gentoo,Biscoe,45.5,13.7,214,4650,FEMALE +Gentoo,Biscoe,48.4,14.6,213,5850,MALE +Gentoo,Biscoe,45.8,14.6,210,4200,FEMALE +Gentoo,Biscoe,49.3,15.7,217,5850,MALE +Gentoo,Biscoe,42,13.5,210,4150,FEMALE +Gentoo,Biscoe,49.2,15.2,221,6300,MALE +Gentoo,Biscoe,46.2,14.5,209,4800,FEMALE +Gentoo,Biscoe,48.7,15.1,222,5350,MALE +Gentoo,Biscoe,50.2,14.3,218,5700,MALE +Gentoo,Biscoe,45.1,14.5,215,5000,FEMALE +Gentoo,Biscoe,46.5,14.5,213,4400,FEMALE +Gentoo,Biscoe,46.3,15.8,215,5050,MALE +Gentoo,Biscoe,42.9,13.1,215,5000,FEMALE +Gentoo,Biscoe,46.1,15.1,215,5100,MALE +Gentoo,Biscoe,44.5,14.3,216,4100, +Gentoo,Biscoe,47.8,15,215,5650,MALE +Gentoo,Biscoe,48.2,14.3,210,4600,FEMALE +Gentoo,Biscoe,50,15.3,220,5550,MALE +Gentoo,Biscoe,47.3,15.3,222,5250,MALE +Gentoo,Biscoe,42.8,14.2,209,4700,FEMALE +Gentoo,Biscoe,45.1,14.5,207,5050,FEMALE +Gentoo,Biscoe,59.6,17,230,6050,MALE +Gentoo,Biscoe,49.1,14.8,220,5150,FEMALE +Gentoo,Biscoe,48.4,16.3,220,5400,MALE +Gentoo,Biscoe,42.6,13.7,213,4950,FEMALE +Gentoo,Biscoe,44.4,17.3,219,5250,MALE +Gentoo,Biscoe,44,13.6,208,4350,FEMALE +Gentoo,Biscoe,48.7,15.7,208,5350,MALE +Gentoo,Biscoe,42.7,13.7,208,3950,FEMALE +Gentoo,Biscoe,49.6,16,225,5700,MALE +Gentoo,Biscoe,45.3,13.7,210,4300,FEMALE +Gentoo,Biscoe,49.6,15,216,4750,MALE +Gentoo,Biscoe,50.5,15.9,222,5550,MALE +Gentoo,Biscoe,43.6,13.9,217,4900,FEMALE +Gentoo,Biscoe,45.5,13.9,210,4200,FEMALE +Gentoo,Biscoe,50.5,15.9,225,5400,MALE +Gentoo,Biscoe,44.9,13.3,213,5100,FEMALE +Gentoo,Biscoe,45.2,15.8,215,5300,MALE +Gentoo,Biscoe,46.6,14.2,210,4850,FEMALE +Gentoo,Biscoe,48.5,14.1,220,5300,MALE +Gentoo,Biscoe,45.1,14.4,210,4400,FEMALE +Gentoo,Biscoe,50.1,15,225,5000,MALE +Gentoo,Biscoe,46.5,14.4,217,4900,FEMALE +Gentoo,Biscoe,45,15.4,220,5050,MALE +Gentoo,Biscoe,43.8,13.9,208,4300,FEMALE +Gentoo,Biscoe,45.5,15,220,5000,MALE +Gentoo,Biscoe,43.2,14.5,208,4450,FEMALE +Gentoo,Biscoe,50.4,15.3,224,5550,MALE +Gentoo,Biscoe,45.3,13.8,208,4200,FEMALE +Gentoo,Biscoe,46.2,14.9,221,5300,MALE +Gentoo,Biscoe,45.7,13.9,214,4400,FEMALE +Gentoo,Biscoe,54.3,15.7,231,5650,MALE +Gentoo,Biscoe,45.8,14.2,219,4700,FEMALE +Gentoo,Biscoe,49.8,16.8,230,5700,MALE +Gentoo,Biscoe,46.2,14.4,214,4650, +Gentoo,Biscoe,49.5,16.2,229,5800,MALE +Gentoo,Biscoe,43.5,14.2,220,4700,FEMALE +Gentoo,Biscoe,50.7,15,223,5550,MALE +Gentoo,Biscoe,47.7,15,216,4750,FEMALE +Gentoo,Biscoe,46.4,15.6,221,5000,MALE +Gentoo,Biscoe,48.2,15.6,221,5100,MALE +Gentoo,Biscoe,46.5,14.8,217,5200,FEMALE +Gentoo,Biscoe,46.4,15,216,4700,FEMALE +Gentoo,Biscoe,48.6,16,230,5800,MALE +Gentoo,Biscoe,47.5,14.2,209,4600,FEMALE +Gentoo,Biscoe,51.1,16.3,220,6000,MALE +Gentoo,Biscoe,45.2,13.8,215,4750,FEMALE +Gentoo,Biscoe,45.2,16.4,223,5950,MALE +Gentoo,Biscoe,49.1,14.5,212,4625,FEMALE +Gentoo,Biscoe,52.5,15.6,221,5450,MALE +Gentoo,Biscoe,47.4,14.6,212,4725,FEMALE +Gentoo,Biscoe,50,15.9,224,5350,MALE +Gentoo,Biscoe,44.9,13.8,212,4750,FEMALE +Gentoo,Biscoe,50.8,17.3,228,5600,MALE +Gentoo,Biscoe,43.4,14.4,218,4600,FEMALE +Gentoo,Biscoe,51.3,14.2,218,5300,MALE +Gentoo,Biscoe,47.5,14,212,4875,FEMALE +Gentoo,Biscoe,52.1,17,230,5550,MALE +Gentoo,Biscoe,47.5,15,218,4950,FEMALE +Gentoo,Biscoe,52.2,17.1,228,5400,MALE +Gentoo,Biscoe,45.5,14.5,212,4750,FEMALE +Gentoo,Biscoe,49.5,16.1,224,5650,MALE +Gentoo,Biscoe,44.5,14.7,214,4850,FEMALE +Gentoo,Biscoe,50.8,15.7,226,5200,MALE +Gentoo,Biscoe,49.4,15.8,216,4925,MALE +Gentoo,Biscoe,46.9,14.6,222,4875,FEMALE +Gentoo,Biscoe,48.4,14.4,203,4625,FEMALE +Gentoo,Biscoe,51.1,16.5,225,5250,MALE +Gentoo,Biscoe,48.5,15,219,4850,FEMALE +Gentoo,Biscoe,55.9,17,228,5600,MALE +Gentoo,Biscoe,47.2,15.5,215,4975,FEMALE +Gentoo,Biscoe,49.1,15,228,5500,MALE +Gentoo,Biscoe,47.3,13.8,216,4725, +Gentoo,Biscoe,46.8,16.1,215,5500,MALE +Gentoo,Biscoe,41.7,14.7,210,4700,FEMALE +Gentoo,Biscoe,53.4,15.8,219,5500,MALE +Gentoo,Biscoe,43.3,14,208,4575,FEMALE +Gentoo,Biscoe,48.1,15.1,209,5500,MALE +Gentoo,Biscoe,50.5,15.2,216,5000,FEMALE +Gentoo,Biscoe,49.8,15.9,229,5950,MALE +Gentoo,Biscoe,43.5,15.2,213,4650,FEMALE +Gentoo,Biscoe,51.5,16.3,230,5500,MALE +Gentoo,Biscoe,46.2,14.1,217,4375,FEMALE +Gentoo,Biscoe,55.1,16,230,5850,MALE +Gentoo,Biscoe,44.5,15.7,217,4875, +Gentoo,Biscoe,48.8,16.2,222,6000,MALE +Gentoo,Biscoe,47.2,13.7,214,4925,FEMALE +Gentoo,Biscoe,NaN,NaN,NaN,NaN, +Gentoo,Biscoe,46.8,14.3,215,4850,FEMALE +Gentoo,Biscoe,50.4,15.7,222,5750,MALE +Gentoo,Biscoe,45.2,14.8,212,5200,FEMALE +Gentoo,Biscoe,49.9,16.1,213,5400,MALE \ No newline at end of file diff --git a/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..9aa296a82 --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/index.md b/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/index.md new file mode 100644 index 000000000..7afc5cca2 --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/06-implicit-marks/index.md @@ -0,0 +1,30 @@ +--- +title: Implicit marks +--- + +You already noticed that SveltePlot adds **axes** to your plot automatically — they are _implicit_ marks that appear without you adding them explicitly. You can disable the implicit axes entirely with `axes={false}`. + +```svelte + +``` + +If you want to add explict axes, just import the `AxisX` and `AxisY` marks and add them to the plot: + +```js +import { Plot, Dot, +++AxisX, AxisY+++ } from 'svelteplot'; +``` + +When adding axes explicitly, you can customize their appearance with props: + +```svelte ++ ++ + +```` +Two more implicit marks are available as shorthand props on ``: `grid` adds grid lines, and `frame` adds a border around the plot area: + +```svelte + +``` + +This is the same as importing `GridX`, `GridY`, and `Frame` and adding them to the plot. diff --git a/src/content/tutorial/01-basics/01-getting-started/index.md b/src/content/tutorial/01-basics/01-getting-started/index.md new file mode 100644 index 000000000..f15cbd374 --- /dev/null +++ b/src/content/tutorial/01-basics/01-getting-started/index.md @@ -0,0 +1,3 @@ +--- +title: Getting Started +--- diff --git a/src/content/tutorial/01-basics/02-scales/+assets/src/lib/aapl.csv b/src/content/tutorial/01-basics/02-scales/+assets/src/lib/aapl.csv new file mode 100644 index 000000000..bcd538da1 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/+assets/src/lib/aapl.csv @@ -0,0 +1,1261 @@ +Date,Open,High,Low,Close,Adj Close,Volume +2013-05-13,64.501427,65.414284,64.5,64.96286,50.961628,79237200 +2013-05-14,64.835716,65.028572,63.164288,63.408573,49.742329,111779500 +2013-05-15,62.737144,63,60.337143,61.264286,48.060188,185403400 +2013-05-16,60.462856,62.549999,59.842857,62.082859,48.702328,150801000 +2013-05-17,62.721428,62.869999,61.572857,61.894287,48.554409,106976100 +2013-05-20,61.701427,63.685715,61.442856,63.275715,49.638096,112894600 +2013-05-21,62.592857,63.639999,62.028572,62.808571,49.271629,114005500 +2013-05-22,63.435715,64.050003,62.602856,63.049999,49.461025,110759600 +2013-05-23,62.278572,63.737144,62.255714,63.162857,49.549564,88255300 +2013-05-24,62.978573,63.665714,62.908573,63.592857,49.886887,69041700 +2013-05-28,64.271431,64.444283,62.978573,63.062859,49.471127,96536300 +2013-05-29,62.857143,63.92857,62.771427,63.564285,49.864471,82644100 +2013-05-30,63.664288,64.928574,63.501427,64.511429,50.607475,88379900 +2013-05-31,64.64286,65.300003,64.214287,64.247147,50.400158,96075700 +2013-06-03,64.389999,64.622856,63.21143,64.388573,50.511097,93088100 +2013-06-04,64.745712,64.918571,63.912857,64.187141,50.353092,73182200 +2013-06-05,63.664288,64.388573,63.387142,63.587143,49.882401,72647400 +2013-06-06,63.638573,63.857143,62.007141,62.637142,49.137157,104233500 +2013-06-07,62.357143,63.32,61.824287,63.115715,49.512577,101133900 +2013-06-10,63.532856,64.154289,62.400002,62.69857,49.185345,112538300 +2013-06-11,62.248573,63.251427,61.902859,62.514286,49.040783,71528100 +2013-06-12,62.785713,63.035713,61.642857,61.741428,48.434486,66306800 +2013-06-13,61.785713,62.44857,61.25,62.279999,48.856991,71458100 +2013-06-14,62.200001,62.327145,61.214287,61.435715,48.194675,67966500 +2013-06-17,61.634285,62.242859,61.48,61.714287,48.4132,64853600 +2013-06-18,61.651428,62.128571,61.458572,61.681427,48.387424,48756400 +2013-06-19,61.628571,61.665714,60.42857,60.42857,47.404587,77735000 +2013-06-20,59.900002,60.854286,59.310001,59.548573,46.714264,89327700 +2013-06-21,59.784286,60,58.299999,59.07143,46.339951,120279600 +2013-06-24,58.200001,58.380001,56.864285,57.505714,45.111698,120186500 +2013-06-25,57.957142,58.255714,56.975716,57.51857,45.121765,78540700 +2013-06-26,57.700001,57.827145,56.522858,56.867142,44.610741,91931000 +2013-06-27,57.035713,57.341427,56.220001,56.254284,44.129971,84311500 +2013-06-28,55.908573,57.181427,55.552856,56.647144,44.438164,144629100 +2013-07-01,57.527142,58.895714,57.317142,58.459999,45.860298,97763400 +2013-07-02,58.565716,60.232857,58.495716,59.784286,46.899162,117466300 +2013-07-03,60.122856,60.425713,59.635715,60.114285,47.158039,60232200 +2013-07-05,60.055714,60.470001,59.335712,59.631428,46.779251,68506200 +2013-07-08,60.015713,60.142857,58.664288,59.292858,46.51366,74534600 +2013-07-09,59.085712,60.5,58.625713,60.335712,47.331741,88146100 +2013-07-10,59.942856,60.685715,59.75,60.104286,47.150196,70351400 +2013-07-11,60.421429,61.17857,60.167141,61.041428,47.885357,81573100 +2013-07-12,61.092857,61.398571,60.487144,60.93,47.797943,69890800 +2013-07-15,60.715714,61.637142,60.685715,61.062859,47.902172,60479300 +2013-07-16,60.931427,61.529999,60.595715,61.457142,48.211479,54134500 +2013-07-17,61.385715,61.745716,61.174286,61.472858,48.223812,49747600 +2013-07-18,61.91143,62.124287,61.515713,61.68,48.386299,54719700 +2013-07-19,61.871429,61.997143,60.621429,60.707142,47.623127,67180400 +2013-07-22,61.351429,61.392857,60.781429,60.901428,47.775539,51949100 +2013-07-23,60.857143,60.994286,59.815716,59.855713,46.955208,92348900 +2013-07-24,62.704285,63.512856,62.18,62.93,49.366905,147984200 +2013-07-25,62.957142,63.057144,62.258572,62.642857,49.14164,57373400 +2013-07-26,62.185715,63.005714,62.048573,62.998573,49.420692,50038100 +2013-07-29,62.971428,64.284286,62.885715,63.970001,50.182755,62014400 +2013-07-30,64.279999,65.307144,64.175713,64.760002,50.802479,77355600 +2013-07-31,64.998573,65.334282,64.204285,64.647141,50.713947,80739400 +2013-08-01,65.10714,65.257141,64.751427,65.239998,51.17902,51562700 +2013-08-02,65.43,66.121429,65.237144,66.077141,51.835732,68695900 +2013-08-05,66.384285,67.238571,66.021431,67.064285,52.610134,79713900 +2013-08-06,66.860001,67.412857,66.024284,66.464287,52.139446,83714400 +2013-08-07,66.257141,66.714287,65.96714,66.425713,52.109192,74714500 +2013-08-08,66.265717,66.300003,65.421425,65.858574,54.150658,63944300 +2013-08-09,65.519997,65.779999,64.807144,64.921425,53.380112,66716300 +2013-08-12,65.265717,66.949997,65.232857,66.765717,54.896542,91108500 +2013-08-13,67.277145,70.665718,66.864288,69.938568,57.505344,220485300 +2013-08-14,71.125717,72.035713,70.485718,71.214287,58.554276,189093100 +2013-08-15,70.917145,71.771431,69.868568,71.129997,58.484966,122573500 +2013-08-16,71.449997,71.848572,71.265717,71.761429,59.004147,90576500 +2013-08-19,72.048569,73.391426,72,72.534286,59.639614,127629600 +2013-08-20,72.815712,72.938568,71.545715,71.581429,58.856155,89672100 +2013-08-21,71.941429,72.449997,71.599998,71.765717,59.007671,83969900 +2013-08-22,72.139999,72.227142,71.171425,71.851425,59.078148,61051900 +2013-08-23,71.895714,71.907143,71.335716,71.574287,58.850273,55682900 +2013-08-26,71.535713,72.885712,71.5,71.852859,59.079327,82741400 +2013-08-27,71.14286,71.78714,69.471428,69.798569,57.390232,106047200 +2013-08-28,69.428574,70.828575,69.428574,70.128571,57.661564,76902000 +2013-08-29,70.235718,70.928574,70.16143,70.242859,57.755524,59914400 +2013-08-30,70.285713,70.421425,69.5,69.602859,57.229309,68074300 +2013-09-03,70.442856,71.514282,69.621429,69.797142,57.389057,82982200 +2013-09-04,71.365715,71.748573,70.897141,71.241432,58.576588,86258200 +2013-09-05,71.464287,71.525711,70.519997,70.752853,58.174866,59091900 +2013-09-06,71.205711,71.339996,69.992859,71.174286,58.521381,89881400 +2013-09-09,72.14286,72.559998,71.925713,72.309998,59.455212,85171800 +2013-09-10,72.314285,72.492859,69.928574,70.662857,58.100861,185798900 +2013-09-11,66.715714,67.669998,66.401428,66.815712,54.937645,224674100 +2013-09-12,66.928574,67.914284,66.572861,67.527145,55.522602,101012800 +2013-09-13,67.048569,67.404289,66.385712,66.414284,54.607578,74708900 +2013-09-16,65.85714,65.944283,63.888573,64.302856,52.87151,135926700 +2013-09-17,63.994286,65.672859,63.92857,65.045715,53.4823,99845200 +2013-09-18,66.168571,66.621429,65.808571,66.382858,54.581741,114215500 +2013-09-19,67.242859,67.975716,67.035713,67.471428,55.476803,101135300 +2013-09-20,68.285713,68.364288,66.571426,66.772858,54.902412,174825700 +2013-09-23,70.871429,70.987144,68.942856,70.091431,57.631035,190526700 +2013-09-24,70.697144,70.781425,69.688568,69.871429,57.450146,91086100 +2013-09-25,69.885712,69.94857,68.775711,68.790001,56.560963,79239300 +2013-09-26,69.428574,69.794289,69.128571,69.459999,57.111843,59305400 +2013-09-27,69.111427,69.238571,68.674286,68.964287,56.704269,57010100 +2013-09-30,68.178574,68.808571,67.772858,68.10714,55.999493,65039100 +2013-10-01,68.349998,69.877144,68.339996,69.708572,57.316231,88470900 +2013-10-02,69.375717,70.257141,69.10714,69.937141,57.504166,72296000 +2013-10-03,70.072861,70.335716,68.677139,69.058571,56.781784,80688300 +2013-10-04,69.122856,69.228569,68.371429,69.004288,56.737148,64717100 +2013-10-07,69.508568,70.378571,69.335716,69.678574,57.291565,78073100 +2013-10-08,69.991432,70.091431,68.648575,68.705711,56.491661,72729300 +2013-10-09,69.234283,69.684288,68.325714,69.512856,57.155315,75431300 +2013-10-10,70.188568,70.339996,69.577141,69.94857,57.513561,69650700 +2013-10-11,69.57,70.548569,69.308571,70.401428,57.885921,66934700 +2013-10-14,69.975716,71.082855,69.907143,70.862854,58.265316,65474500 +2013-10-15,71.072861,71.714287,70.788574,71.239998,58.575417,80018400 +2013-10-16,71.541428,71.790001,71.318573,71.587143,58.860844,62775300 +2013-10-17,71.425713,72.111427,71.382858,72.071426,59.259037,63398300 +2013-10-18,72.284286,72.751427,72.244286,72.69857,59.774693,72635500 +2013-10-21,73.110001,74.900002,73.074287,74.480003,61.239429,99526700 +2013-10-22,75.201431,75.492859,72.575714,74.267143,61.0644,133515900 +2013-10-23,74.14286,75.095711,74.14286,74.994286,61.662289,78430800 +2013-10-24,75,76.067146,74.635712,75.987144,62.478638,96191200 +2013-10-25,75.902855,76.175713,75.015717,75.137146,61.779747,84448000 +2013-10-28,75.577141,75.85714,74.744286,75.697144,62.240196,137610200 +2013-10-29,76.610001,77.035713,73.505714,73.811432,60.689693,158951800 +2013-10-30,74.230003,75.360001,73.860001,74.985718,61.655243,88540900 +2013-10-31,75,75.355713,74.46714,74.671425,61.396824,68924100 +2013-11-01,74.860001,74.971428,73.691429,74.290001,61.08321,68722500 +2013-11-04,74.442856,75.260002,74.115715,75.25,61.872543,61156900 +2013-11-05,74.940002,75.555717,74.714287,75.064285,61.719852,66303300 +2013-11-06,74.878571,74.980003,74.028572,74.417145,63.779209,55843900 +2013-11-07,74.225716,74.741432,73.197144,73.21286,62.747082,65655100 +2013-11-08,73.511429,74.447144,73.227142,74.365715,63.735111,69829200 +2013-11-11,74.284286,74.524284,73.487144,74.150002,63.550251,56863100 +2013-11-12,73.952858,74.845711,73.85714,74.28714,63.667797,51069200 +2013-11-13,74,74.60714,73.851425,74.375717,63.743702,49305200 +2013-11-14,74.687141,75.611427,74.552856,75.451431,64.665649,70604800 +2013-11-15,75.225716,75.584282,74.927139,74.998573,64.277519,79480100 +2013-11-18,74.998573,75.312859,74.028572,74.089996,63.498821,61236000 +2013-11-19,74.147141,74.76857,73.995712,74.221428,63.611477,52234700 +2013-11-20,74.175713,74.345711,73.475716,73.571426,63.054394,48479200 +2013-11-21,73.942856,74.458572,73.381432,74.44857,63.806152,65506700 +2013-11-22,74.21714,74.594284,74.075714,74.257141,63.642082,55931400 +2013-11-25,74.431427,75.124283,74.428574,74.82,64.124481,57327900 +2013-11-26,74.874283,76.591431,74.85714,76.199997,65.307205,100345700 +2013-11-27,76.615715,78,76.199997,77.994286,66.845009,90862100 +2013-11-29,78.497147,79.761429,78.258568,79.438568,68.082802,79531900 +2013-12-02,79.714287,80.618568,78.688568,78.747147,67.490242,118136200 +2013-12-03,79.757141,80.91143,79.668571,80.902855,69.337791,112742000 +2013-12-04,80.785713,81.312859,80.117142,80.714287,69.17617,94452400 +2013-12-05,81.807144,82.162857,80.915718,81.128571,69.531235,111895000 +2013-12-06,80.827141,80.964287,79.938568,80.002853,68.566452,86088100 +2013-12-09,80.128571,81.368568,80.128571,80.918571,69.35125,80123400 +2013-12-10,80.511429,81.125717,80.171425,80.792854,69.24353,69567400 +2013-12-11,81,81.567146,79.955711,80.194283,68.730499,89929700 +2013-12-12,80.305717,80.762856,80.004288,80.077141,68.630119,65572500 +2013-12-13,80.407143,80.41143,79.095711,79.204285,67.882027,83205500 +2013-12-16,79.288574,80.377144,79.28714,79.64286,68.257927,70648200 +2013-12-17,79.401428,79.919998,79.054283,79.284286,67.950592,57475600 +2013-12-18,78.528572,78.778572,76.971428,78.681427,67.433922,141465800 +2013-12-19,78.5,78.571426,77.675713,77.779999,66.661339,80077200 +2013-12-20,77.918571,78.80143,77.831429,78.431427,67.21965,109103400 +2013-12-23,81.14286,81.531425,80.394287,81.441429,69.799385,125326600 +2013-12-24,81.412857,81.697144,80.861427,81.095711,69.50309,41888700 +2013-12-26,81.157143,81.35714,80.482857,80.557144,69.041496,51002000 +2013-12-27,80.545715,80.629997,79.928574,80.012856,68.574997,56471100 +2013-12-30,79.637146,80.012856,78.902855,79.21714,67.893066,63407400 +2013-12-31,79.167145,80.182854,79.14286,80.145714,68.688881,55771100 +2014-01-02,79.382858,79.575714,78.860001,79.01857,67.722862,58671200 +2014-01-03,78.980003,79.099998,77.204285,77.28286,66.235268,98116900 +2014-01-06,76.778572,78.114288,76.228569,77.704285,66.596466,103152700 +2014-01-07,77.760002,77.994286,76.845711,77.148575,66.120186,79302300 +2014-01-08,76.972855,77.937141,76.955711,77.637146,66.538902,64632400 +2014-01-09,78.114288,78.122856,76.478569,76.645714,65.689217,69787200 +2014-01-10,77.118568,77.257141,75.872856,76.134285,65.25087,76244000 +2014-01-13,75.701431,77.5,75.697144,76.53286,65.592491,94623200 +2014-01-14,76.888573,78.104286,76.808571,78.055717,66.897652,83140400 +2014-01-15,79.074287,80.028572,78.808571,79.622856,68.240753,97909700 +2014-01-16,79.271431,79.550003,78.811432,79.178574,67.859993,57319500 +2014-01-17,78.78286,78.867142,77.128571,77.238571,66.197319,106684900 +2014-01-21,77.284286,78.581429,77.202858,78.438568,67.225777,82131700 +2014-01-22,78.701431,79.612854,78.258568,78.78714,67.524521,94996300 +2014-01-23,78.562859,79.5,77.830002,79.454285,68.096291,100809800 +2014-01-24,79.14286,79.374283,77.821426,78.010002,66.858467,107338700 +2014-01-27,78.581429,79.257141,77.964287,78.64286,67.400864,138719700 +2014-01-28,72.68,73.571426,71.724289,72.35714,62.013683,266380800 +2014-01-29,71.992859,72.48143,71.23143,71.535713,61.309677,125702500 +2014-01-30,71.791428,72.35714,70.957146,71.397141,61.190922,169625400 +2014-01-31,70.739998,71.647141,70.507141,71.514282,61.291309,116199300 +2014-02-03,71.80143,72.53286,71.328575,71.647141,61.405186,100366000 +2014-02-04,72.264282,72.779999,71.822861,72.684288,62.294071,94170300 +2014-02-05,72.365715,73.611427,72.321426,73.227142,62.759319,82086200 +2014-02-06,72.865715,73.35714,72.544289,73.215714,65.476723,64441300 +2014-02-07,74.482857,74.704285,73.91143,74.239998,66.392738,92570100 +2014-02-10,74.094284,75.998573,74,75.57,67.582138,86389800 +2014-02-11,75.80143,76.821426,75.64286,76.565712,68.472603,70564200 +2014-02-12,76.707146,77.080002,76.177139,76.559998,68.467506,77025200 +2014-02-13,76.379997,77.835716,76.314285,77.775711,69.554703,76849500 +2014-02-14,77.495712,77.997147,77.315712,77.71286,69.49852,68231100 +2014-02-18,78,78.741432,77.944283,77.998573,69.754028,65062900 +2014-02-19,77.821426,78.127144,76.335716,76.767143,68.652756,78442000 +2014-02-20,76.141426,76.714287,75.571426,75.878571,67.858101,76464500 +2014-02-21,76.112854,76.367142,74.942856,75.035713,67.10434,69696200 +2014-02-24,74.735718,75.702858,74.631432,75.364288,67.398178,72227400 +2014-02-25,75.625717,75.652855,74.428574,74.580002,66.696777,57988000 +2014-02-26,74.80143,75,73.657143,73.907143,66.095062,69054300 +2014-02-27,73.877144,75.540001,73.721428,75.381432,67.413513,75470500 +2014-02-28,75.582855,76.10714,74.58857,75.177139,67.230804,92992200 +2014-03-03,74.774284,75.807144,74.687141,75.394287,67.424995,59695300 +2014-03-04,75.85714,76.091431,75.395714,75.891426,67.869591,64785000 +2014-03-05,75.845711,76.39286,75.589996,76.05143,68.01268,50015700 +2014-03-06,76.112854,76.348572,75.442856,75.821426,67.806984,46372200 +2014-03-07,75.870003,75.997147,75.150002,75.777145,67.767372,55182400 +2014-03-10,75.480003,76.190002,75.477142,75.845711,67.82872,44646000 +2014-03-11,76.492859,76.96286,76.084282,76.584282,68.48922,69806100 +2014-03-12,76.358574,76.764282,76,76.658569,68.555649,49831600 +2014-03-13,76.777145,77.094284,75.594284,75.807144,67.794228,64435700 +2014-03-14,75.541428,75.841431,74.714287,74.955711,67.032784,59299800 +2014-03-17,75.385712,75.709999,75.121429,75.248573,67.294701,49886200 +2014-03-18,75.128571,75.995712,75.028572,75.914284,67.89003,52411800 +2014-03-19,76.03714,76.605713,75.571426,75.894287,67.872169,56189000 +2014-03-20,75.69857,76.095711,75.335716,75.528572,67.54509,52099600 +2014-03-21,75.989998,76.25,75.190002,76.124283,68.077827,93511600 +2014-03-24,76.917145,77.214287,76.437141,77.027145,68.885262,88925200 +2014-03-25,77.35714,77.964287,77.084282,77.855713,69.626259,70573300 +2014-03-26,78.074287,78.428574,76.980003,77.111427,68.96064,74942000 +2014-03-27,77.145714,77.35714,76.445717,76.779999,68.664246,55507900 +2014-03-28,76.902855,76.991432,76.321426,76.694283,68.587601,50141000 +2014-03-31,77.03286,77.258568,76.561432,76.677139,68.572273,42167300 +2014-04-01,76.822861,77.410004,76.681427,77.378571,69.199539,50190000 +2014-04-02,77.482857,77.639999,77.18,77.507141,69.314529,45105200 +2014-04-03,77.341431,77.5,76.805717,76.970001,68.834145,40586000 +2014-04-04,77.115715,77.14286,75.797142,75.974289,67.943726,68812800 +2014-04-07,75.431427,75.842857,74.555717,74.781425,66.87693,72462600 +2014-04-08,75.027145,75.160004,74.099998,74.777145,66.8731,60972100 +2014-04-09,74.662857,75.784286,74.574287,75.760002,67.75206,51542400 +2014-04-10,75.811432,76.034286,74.738571,74.78286,66.878227,59913000 +2014-04-11,74.14286,74.690002,73.877144,74.230003,66.383789,67929400 +2014-04-14,74.557144,74.594284,73.887146,74.525711,66.648232,51418500 +2014-04-15,74.324287,74.519997,73.047142,73.994286,66.172997,66622500 +2014-04-16,74.007141,74.441429,73.44857,74.144287,66.307129,53691400 +2014-04-17,74.285713,75.394287,74.171425,74.991432,67.064743,71083600 +2014-04-21,75.048569,76.019997,74.851425,75.881432,67.860657,45637200 +2014-04-22,75.472855,75.975716,75.214287,75.957146,67.928375,50640800 +2014-04-23,75.580002,75.875717,74.921425,74.964287,67.040466,98735000 +2014-04-24,81.172859,81.428574,80.104286,81.110001,72.536575,189977900 +2014-04-25,80.647141,81.71286,80.565712,81.705711,73.069313,97568800 +2014-04-28,81.828575,85.10714,81.792854,84.870003,75.899139,167371400 +2014-04-29,84.82,85.139999,84.215714,84.618568,75.674263,84344400 +2014-04-30,84.662857,85.632858,84.257141,84.298569,75.3881,114160200 +2014-05-01,84.571426,84.971428,83.765717,84.497147,75.565689,61012000 +2014-05-02,84.620003,84.885712,84.244286,84.654289,75.706215,47878600 +2014-05-05,84.305717,85.85714,84.285713,85.851425,76.776825,71766800 +2014-05-06,85.971428,86.344284,84.915718,84.915718,75.940018,93641100 +2014-05-07,85.035713,85.327141,83.961426,84.618568,75.674263,70716100 +2014-05-08,84.035713,84.915718,83.771431,83.998573,78.15863,57574300 +2014-05-09,83.505714,83.75,82.904289,83.648575,77.832993,72899400 +2014-05-12,83.927139,84.808571,83.914284,84.690002,78.801994,53302200 +2014-05-13,84.571426,84.934288,84.385712,84.822861,78.925613,39934300 +2014-05-14,84.632858,85.342857,84.534286,84.83857,78.940239,41601000 +2014-05-15,84.957146,85.228569,84.005714,84.117142,78.268967,57711500 +2014-05-16,84.089996,85.361427,83.628571,85.358574,79.424072,69064100 +2014-05-19,85.407143,86.761429,85.332855,86.370003,80.365196,79438800 +2014-05-20,86.358574,86.628571,85.818573,86.387146,80.381149,58709000 +2014-05-21,86.261429,86.671425,86.008568,86.615715,80.593834,49214900 +2014-05-22,86.657143,87.121429,86.300003,86.752853,80.721428,50190000 +2014-05-23,86.75,87.818573,86.638573,87.732857,81.633316,58052400 +2014-05-27,87.982857,89.408569,87.947144,89.375717,83.161942,87216500 +2014-05-28,89.431427,89.975716,89.111427,89.144287,82.946617,78870400 +2014-05-29,89.692856,90.98143,89.681427,90.76857,84.457962,94118500 +2014-05-30,91.139999,92.024284,89.842857,90.428574,84.141617,141005200 +2014-06-02,90.565712,90.690002,88.928574,89.807144,83.563377,92337700 +2014-06-03,89.779999,91.248573,89.75,91.077141,84.745071,73177300 +2014-06-04,91.062859,92.555717,90.872856,92.117142,85.712769,83870500 +2014-06-05,92.314285,92.767143,91.80143,92.478569,86.049088,75951400 +2014-06-06,92.842857,93.03714,92.067146,92.224289,85.812477,87484600 +2014-06-09,92.699997,93.879997,91.75,93.699997,87.185577,75415000 +2014-06-10,94.730003,95.050003,93.57,94.25,87.697365,62777000 +2014-06-11,94.129997,94.760002,93.470001,93.860001,87.334457,45681000 +2014-06-12,94.040001,94.120003,91.900002,92.290001,85.873611,54749000 +2014-06-13,92.199997,92.440002,90.879997,91.279999,84.933846,54525000 +2014-06-16,91.510002,92.75,91.449997,92.199997,85.789871,35561000 +2014-06-17,92.309998,92.699997,91.800003,92.080002,85.678207,29726000 +2014-06-18,92.269997,92.290001,91.349998,92.18,85.771263,33514000 +2014-06-19,92.290001,92.300003,91.339996,91.860001,85.473503,35528000 +2014-06-20,91.849998,92.550003,90.900002,90.910004,84.589569,100898000 +2014-06-23,91.32,91.620003,90.599998,90.830002,84.515114,43694000 +2014-06-24,90.75,91.739998,90.190002,90.279999,84.003365,39036000 +2014-06-25,90.209999,90.699997,89.650002,90.360001,84.077789,36869000 +2014-06-26,90.370003,91.050003,89.800003,90.900002,84.580269,32629000 +2014-06-27,90.82,92,90.769997,91.980003,85.585167,64029000 +2014-06-30,92.099998,93.730003,92.089996,92.93,86.469116,49482300 +2014-07-01,93.519997,94.07,93.129997,93.519997,87.018105,38223000 +2014-07-02,93.870003,94.059998,93.089996,93.480003,86.980873,28465000 +2014-07-03,93.669998,94.099998,93.199997,94.029999,87.49263,22891800 +2014-07-07,94.139999,95.989998,94.099998,95.970001,89.297775,56468000 +2014-07-08,96.269997,96.800003,93.919998,95.349998,88.720863,65222000 +2014-07-09,95.440002,95.949997,94.760002,95.389999,88.758087,36436000 +2014-07-10,93.760002,95.550003,93.519997,95.040001,88.432434,39686000 +2014-07-11,95.360001,95.889999,94.860001,95.220001,88.599915,34018000 +2014-07-14,95.860001,96.889999,95.650002,96.449997,89.744392,42810000 +2014-07-15,96.800003,96.849998,95.029999,95.32,88.692963,45477900 +2014-07-16,96.970001,97.099998,94.739998,94.779999,88.190491,53396300 +2014-07-17,95.029999,95.279999,92.57,93.089996,86.618011,57298000 +2014-07-18,93.620003,94.739998,93.019997,94.43,87.864822,49988000 +2014-07-21,94.989998,95,93.720001,93.940002,87.408913,39079000 +2014-07-22,94.68,94.889999,94.120003,94.720001,88.134674,55197000 +2014-07-23,95.419998,97.879997,95.169998,97.190002,90.432945,92918000 +2014-07-24,97.040001,97.32,96.419998,97.029999,90.284065,45729000 +2014-07-25,96.849998,97.839996,96.639999,97.669998,90.87957,43469000 +2014-07-28,97.82,99.239998,97.550003,99.019997,92.135719,55318000 +2014-07-29,99.330002,99.440002,98.25,98.379997,91.540215,43143000 +2014-07-30,98.440002,98.699997,97.669998,98.150002,91.326195,33010000 +2014-07-31,97.160004,97.449997,95.330002,95.599998,88.953499,56843000 +2014-08-01,94.900002,96.620003,94.809998,96.129997,89.44664,48511000 +2014-08-04,96.370003,96.580002,95.169998,95.589996,88.944183,39958000 +2014-08-05,95.360001,95.68,94.360001,95.120003,88.506859,55933000 +2014-08-06,94.75,95.480003,94.709999,94.959999,88.357979,38558000 +2014-08-07,94.93,95.949997,94.099998,94.480003,88.348633,46711000 +2014-08-08,94.260002,94.82,93.279999,94.739998,88.591759,41865000 +2014-08-11,95.269997,96.080002,94.839996,95.989998,89.760643,36585000 +2014-08-12,96.040001,96.879997,95.610001,95.970001,89.741943,33795000 +2014-08-13,96.150002,97.239998,96.040001,97.239998,90.929512,31916000 +2014-08-14,97.330002,97.57,96.800003,97.5,91.172653,28116000 +2014-08-15,97.900002,98.190002,96.860001,97.980003,91.621513,48951000 +2014-08-18,98.489998,99.370003,97.980003,99.160004,92.724922,47572000 +2014-08-19,99.410004,100.68,99.32,100.529999,94.006012,69399000 +2014-08-20,100.440002,101.089996,99.949997,100.57,94.043434,52699000 +2014-08-21,100.57,100.940002,100.110001,100.580002,94.05278,33478000 +2014-08-22,100.290001,101.470001,100.190002,101.32,94.744743,44184000 +2014-08-25,101.790001,102.169998,101.279999,101.540001,94.95047,40270000 +2014-08-26,101.419998,101.5,100.860001,100.889999,94.342651,33152000 +2014-08-27,101.019997,102.57,100.699997,102.129997,95.502167,52369000 +2014-08-28,101.589996,102.779999,101.559998,102.25,95.614388,68460000 +2014-08-29,102.860001,102.900002,102.199997,102.5,95.848152,44595000 +2014-09-02,103.059998,103.739998,102.720001,103.300003,96.59626,53564000 +2014-09-03,103.099998,103.199997,98.580002,98.940002,92.519188,125421000 +2014-09-04,98.849998,100.089996,97.790001,98.120003,91.752419,85718000 +2014-09-05,98.800003,99.389999,98.309998,98.970001,92.547249,58457000 +2014-09-08,99.300003,99.309998,98.050003,98.360001,91.976845,46356700 +2014-09-09,99.080002,103.080002,96.139999,97.989998,91.630852,189846300 +2014-09-10,98.010002,101.110001,97.760002,101,94.445526,100869600 +2014-09-11,100.410004,101.440002,99.620003,101.43,94.847603,62353100 +2014-09-12,101.209999,102.190002,101.080002,101.660004,95.062691,62626100 +2014-09-15,102.809998,103.050003,101.440002,101.629997,95.034615,61316500 +2014-09-16,99.800003,101.260002,98.889999,100.860001,94.314598,66908100 +2014-09-17,101.269997,101.800003,100.589996,101.580002,94.987869,60926500 +2014-09-18,101.93,102.349998,101.559998,101.790001,95.18425,37299400 +2014-09-19,102.290001,102.349998,100.5,100.959999,94.408112,70902400 +2014-09-22,101.800003,102.139999,100.580002,101.059998,94.501625,52788400 +2014-09-23,100.599998,102.940002,100.540001,102.639999,95.979088,63402200 +2014-09-24,102.160004,102.849998,101.199997,101.75,95.146843,60171800 +2014-09-25,100.510002,100.709999,97.720001,97.870003,91.518661,100092000 +2014-09-26,98.529999,100.75,98.400002,100.75,94.211754,62370500 +2014-09-29,98.650002,100.440002,98.629997,100.110001,93.613274,49766300 +2014-09-30,100.809998,101.540001,100.529999,100.75,94.211754,55264100 +2014-10-01,100.589996,100.690002,98.699997,99.18,92.743637,51491300 +2014-10-02,99.269997,100.220001,98.040001,99.900002,93.416908,47757800 +2014-10-03,99.440002,100.209999,99.040001,99.620003,93.155067,43469600 +2014-10-06,99.949997,100.650002,99.419998,99.620003,93.155067,37051200 +2014-10-07,99.43,100.120003,98.730003,98.75,92.341522,42094200 +2014-10-08,98.760002,101.110001,98.309998,100.800003,94.258507,57404700 +2014-10-09,101.540001,102.379997,100.610001,101.019997,94.464211,77376500 +2014-10-10,100.690002,102.029999,100.300003,100.730003,94.193039,66331600 +2014-10-13,101.330002,101.779999,99.809998,99.809998,93.332733,53583400 +2014-10-14,100.389999,100.519997,98.57,98.75,92.341522,63688600 +2014-10-15,97.970001,99.150002,95.18,97.540001,91.210045,100933600 +2014-10-16,95.550003,97.720001,95.410004,96.260002,90.013115,72154500 +2014-10-17,97.5,99,96.809998,97.669998,91.331619,68179700 +2014-10-20,98.32,99.959999,98.220001,99.760002,93.285973,77517300 +2014-10-21,103.019997,103.019997,101.269997,102.470001,95.820122,94623900 +2014-10-22,102.839996,104.110001,102.599998,102.989998,96.306366,68263100 +2014-10-23,104.080002,105.050003,103.629997,104.830002,98.026962,71074700 +2014-10-24,105.18,105.489998,104.529999,105.220001,98.391663,47053900 +2014-10-27,104.849998,105.480003,104.699997,105.110001,98.28878,34187700 +2014-10-28,105.400002,106.739998,105.349998,106.739998,99.813004,48060900 +2014-10-29,106.650002,107.370003,106.360001,107.339996,100.374069,52687900 +2014-10-30,106.959999,107.349998,105.900002,106.980003,100.037453,40654800 +2014-10-31,108.010002,108.040001,107.209999,108,100.991234,44639300 +2014-11-03,108.220001,110.300003,108.010002,109.400002,102.300392,52282600 +2014-11-04,109.360001,109.489998,107.720001,108.599998,101.552292,41574400 +2014-11-05,109.099998,109.300003,108.129997,108.860001,101.795433,37435900 +2014-11-06,108.599998,108.790001,107.800003,108.699997,102.086563,34968500 +2014-11-07,108.75,109.32,108.550003,109.010002,102.377716,33691500 +2014-11-10,109.019997,109.330002,108.669998,108.830002,102.208649,27195500 +2014-11-11,108.699997,109.75,108.400002,109.699997,103.025711,27442300 +2014-11-12,109.379997,111.43,109.370003,111.25,104.481422,46942400 +2014-11-13,111.800003,113.449997,111.599998,112.82,105.955902,59522900 +2014-11-14,113.150002,114.190002,111.209999,114.18,107.23317,44063600 +2014-11-17,114.269997,117.279999,113.300003,113.989998,107.054726,46746700 +2014-11-18,113.940002,115.690002,113.889999,115.470001,108.444672,44224000 +2014-11-19,115.440002,115.739998,113.800003,114.669998,107.693336,41869200 +2014-11-20,114.910004,116.860001,114.849998,116.309998,109.233559,43395500 +2014-11-21,117.510002,117.57,116.029999,116.470001,109.383842,57179300 +2014-11-24,116.849998,118.769997,116.620003,118.629997,111.412415,47450800 +2014-11-25,119.07,119.75,117.449997,117.599998,110.445091,68840400 +2014-11-26,117.940002,119.099998,117.830002,119,111.759903,40768300 +2014-11-28,119.269997,119.400002,118.050003,118.93,111.69416,24814400 +2014-12-01,118.809998,119.25,111.269997,115.07,108.069008,83814000 +2014-12-02,113.5,115.75,112.75,114.629997,107.655785,59348900 +2014-12-03,115.75,116.349998,115.110001,115.93,108.876686,43063400 +2014-12-04,115.769997,117.199997,115.290001,115.489998,108.463455,42044500 +2014-12-05,115.989998,116.080002,114.639999,115,108.003265,38318900 +2014-12-08,114.099998,114.650002,111.620003,112.400002,105.561447,57664900 +2014-12-09,110.190002,114.300003,109.349998,114.120003,107.176811,60208000 +2014-12-10,114.410004,114.849998,111.540001,111.949997,105.13884,44565300 +2014-12-11,112.260002,113.800003,111.339996,111.620003,104.828918,41401700 +2014-12-12,110.459999,111.870003,109.580002,109.730003,103.053902,56028100 +2014-12-15,110.699997,111.599998,106.349998,108.230003,101.645164,67218100 +2014-12-16,106.370003,110.160004,106.260002,106.75,100.255203,60790700 +2014-12-17,107.120003,109.839996,106.82,109.410004,102.753387,53411800 +2014-12-18,111.870003,112.650002,110.660004,112.650002,105.796242,59006200 +2014-12-19,112.260002,113.239998,111.660004,111.779999,104.979164,88429800 +2014-12-22,112.160004,113.489998,111.970001,112.940002,106.068596,45167500 +2014-12-23,113.230003,113.330002,112.459999,112.540001,105.69294,26028400 +2014-12-24,112.580002,112.709999,112.010002,112.010002,105.19519,14479600 +2014-12-26,112.099998,114.519997,112.010002,113.989998,107.054726,33721000 +2014-12-29,113.790001,114.769997,113.699997,113.910004,106.979584,27598900 +2014-12-30,113.639999,113.919998,112.110001,112.519997,105.674149,29881500 +2014-12-31,112.82,113.129997,110.209999,110.379997,103.664352,41403400 +2015-01-02,111.389999,111.440002,107.349998,109.330002,102.67823,53204600 +2015-01-05,108.290001,108.650002,105.410004,106.25,99.785629,64285500 +2015-01-06,106.540001,107.43,104.629997,106.260002,99.795021,65797100 +2015-01-07,107.199997,108.199997,106.699997,107.75,101.194359,40105900 +2015-01-08,109.230003,112.150002,108.699997,111.889999,105.082497,59364500 +2015-01-09,112.669998,113.25,110.209999,112.010002,105.19519,53699500 +2015-01-12,112.599998,112.629997,108.800003,109.25,102.603104,49650800 +2015-01-13,111.43,112.800003,108.910004,110.220001,103.514084,67091900 +2015-01-14,109.040001,110.489998,108.5,109.800003,103.119652,48337000 +2015-01-15,110,110.059998,106.660004,106.82,100.320961,60014000 +2015-01-16,107.029999,107.580002,105.199997,105.989998,99.541428,78513300 +2015-01-20,107.839996,108.970001,106.5,108.720001,102.105339,49899900 +2015-01-21,108.949997,111.059998,108.269997,109.550003,102.884872,48575900 +2015-01-22,110.260002,112.470001,109.720001,112.400002,105.561447,53796400 +2015-01-23,112.300003,113.75,111.529999,112.980003,106.106171,46464800 +2015-01-26,113.739998,114.360001,112.800003,113.099998,106.218864,55615000 +2015-01-27,112.419998,112.480003,109.029999,109.139999,102.499809,95568700 +2015-01-28,117.629997,118.120003,115.309998,115.309998,108.294418,146477100 +2015-01-29,116.32,119.190002,115.559998,118.900002,111.665985,84436400 +2015-01-30,118.400002,120,116.849998,117.160004,110.03186,83745500 +2015-02-02,118.050003,119.169998,116.080002,118.629997,111.412415,62739100 +2015-02-03,118.5,119.089996,117.610001,118.650002,111.431198,51915700 +2015-02-04,118.5,120.510002,118.309998,119.559998,112.285835,70149700 +2015-02-05,120.019997,120.230003,119.25,119.940002,113.087288,42246200 +2015-02-06,120.019997,120.25,118.449997,118.93,112.134979,43706600 +2015-02-09,118.550003,119.839996,118.43,119.720001,112.879837,38889800 +2015-02-10,120.169998,122.150002,120.160004,122.019997,115.048439,62008500 +2015-02-11,122.769997,124.919998,122.5,124.879997,117.745018,73561800 +2015-02-12,126.059998,127.480003,125.57,126.459999,119.234741,74474500 +2015-02-13,127.279999,127.279999,125.650002,127.080002,119.819328,54272200 +2015-02-17,127.489998,128.880005,126.919998,127.830002,120.526489,63152400 +2015-02-18,127.629997,128.779999,127.449997,128.720001,121.365623,44891700 +2015-02-19,128.479996,129.029999,128.330002,128.449997,121.111046,37362400 +2015-02-20,128.619995,129.5,128.050003,129.5,122.101074,48948400 +2015-02-23,130.020004,133,129.660004,133,125.401093,70974100 +2015-02-24,132.940002,133.600006,131.169998,132.169998,124.6185,69228100 +2015-02-25,131.559998,131.600006,128.149994,128.789993,121.431618,74711700 +2015-02-26,128.789993,130.869995,126.610001,130.419998,122.968491,91287500 +2015-02-27,130,130.570007,128.240005,128.460007,121.120499,62014800 +2015-03-02,129.25,130.279999,128.300003,129.089996,121.714493,48096700 +2015-03-03,128.960007,129.520004,128.089996,129.360001,121.969078,37816300 +2015-03-04,129.100006,129.559998,128.320007,128.539993,121.195908,31666300 +2015-03-05,128.580002,128.75,125.760002,126.410004,119.187622,56517100 +2015-03-06,128.399994,129.369995,126.260002,126.599998,119.36676,72842100 +2015-03-09,127.959999,129.570007,125.059998,127.139999,119.875908,88528500 +2015-03-10,126.410004,127.220001,123.800003,124.510002,117.396172,68856600 +2015-03-11,124.75,124.769997,122.110001,122.239998,115.255852,68939000 +2015-03-12,122.309998,124.900002,121.629997,124.449997,117.3396,48362700 +2015-03-13,124.400002,125.400002,122.580002,123.589996,116.528725,51827300 +2015-03-16,123.879997,124.949997,122.870003,124.949997,117.811035,35874300 +2015-03-17,125.900002,127.32,125.650002,127.040001,119.781624,51023100 +2015-03-18,127,129.160004,126.370003,128.470001,121.129906,65270900 +2015-03-19,128.75,129.25,127.400002,127.5,120.21534,45809500 +2015-03-20,128.25,128.399994,125.160004,125.900002,118.706749,68695100 +2015-03-23,127.120003,127.849998,126.519997,127.209999,119.941895,37709700 +2015-03-24,127.230003,128.039993,126.559998,126.690002,119.451622,32842300 +2015-03-25,126.540001,126.82,123.379997,123.379997,116.330727,51655200 +2015-03-26,122.760002,124.879997,122.599998,124.239998,117.141594,47572900 +2015-03-27,124.57,124.699997,122.910004,123.25,116.20816,39546200 +2015-03-30,124.050003,126.400002,124,126.370003,119.14991,47099700 +2015-03-31,126.089996,126.489998,124.360001,124.43,117.32074,42090600 +2015-04-01,124.82,125.120003,123.099998,124.25,117.151024,40621400 +2015-04-02,125.029999,125.559998,124.190002,125.32,118.159889,32220100 +2015-04-06,124.470001,127.510002,124.330002,127.349998,120.073914,37194000 +2015-04-07,127.639999,128.119995,125.980003,126.010002,118.810463,35012300 +2015-04-08,125.849998,126.400002,124.970001,125.599998,118.423904,37329200 +2015-04-09,125.849998,126.580002,124.660004,126.559998,119.329041,32484000 +2015-04-10,125.949997,127.209999,125.260002,127.099998,119.838188,40188000 +2015-04-13,128.369995,128.570007,126.610001,126.849998,119.602478,36365100 +2015-04-14,127,127.290001,125.910004,126.300003,119.083893,25524600 +2015-04-15,126.410004,127.129997,126.010002,126.779999,119.536476,28970400 +2015-04-16,126.279999,127.099998,126.110001,126.169998,118.961311,28369000 +2015-04-17,125.550003,126.139999,124.459999,124.75,117.622444,51957000 +2015-04-20,125.57,128.119995,125.169998,127.599998,120.309624,47054300 +2015-04-21,128.100006,128.199997,126.669998,126.910004,119.65905,32435100 +2015-04-22,126.989998,128.869995,126.32,128.619995,121.271347,37654500 +2015-04-23,128.300003,130.419998,128.139999,129.669998,122.261353,45770900 +2015-04-24,130.490005,130.630005,129.229996,130.279999,122.836502,44525900 +2015-04-27,132.309998,133.130005,131.149994,132.649994,125.071068,96954200 +2015-04-28,134.460007,134.539993,129.570007,130.559998,123.100502,118924000 +2015-04-29,130.160004,131.589996,128.300003,128.639999,121.290192,63386100 +2015-04-30,128.639999,128.639999,124.580002,125.150002,117.999596,83195400 +2015-05-01,126.099998,130.130005,125.300003,128.949997,121.582474,58512600 +2015-05-04,129.5,130.570007,128.259995,128.699997,121.346771,50988300 +2015-05-05,128.149994,128.449997,125.779999,125.800003,118.612465,49271400 +2015-05-06,126.559998,126.75,123.360001,125.010002,117.867599,72141000 +2015-05-07,124.769997,126.080002,124.019997,125.260002,118.596642,43940900 +2015-05-08,126.68,127.620003,126.110001,127.620003,120.831108,55550400 +2015-05-11,127.389999,127.559998,125.629997,126.32,119.600243,42035800 +2015-05-12,125.599998,126.879997,124.82,125.870003,119.174187,48160000 +2015-05-13,126.150002,127.190002,125.870003,126.010002,119.306732,34694200 +2015-05-14,127.410004,128.949997,127.160004,128.949997,122.09034,45203500 +2015-05-15,129.070007,129.490005,128.210007,128.770004,121.919922,38208000 +2015-05-18,128.380005,130.720001,128.360001,130.190002,123.264397,50882900 +2015-05-19,130.690002,130.880005,129.639999,130.070007,123.150787,44633200 +2015-05-20,130,130.979996,129.339996,130.059998,123.141296,36454900 +2015-05-21,130.070007,131.630005,129.830002,131.389999,124.400558,39730400 +2015-05-22,131.600006,132.970001,131.399994,132.539993,125.489357,45596000 +2015-05-26,132.600006,132.910004,129.119995,129.619995,122.724678,70697600 +2015-05-27,130.339996,132.259995,130.050003,132.039993,125.015953,45833200 +2015-05-28,131.860001,131.949997,131.100006,131.779999,124.769791,30733300 +2015-05-29,131.229996,131.449997,129.899994,130.279999,123.349586,50884500 +2015-06-01,130.279999,131.389999,130.050003,130.539993,123.595764,32112800 +2015-06-02,129.860001,130.660004,129.320007,129.960007,123.046638,33667600 +2015-06-03,130.660004,130.940002,129.899994,130.119995,123.198097,30889400 +2015-06-04,129.580002,130.580002,128.910004,129.360001,122.478546,38450100 +2015-06-05,129.5,129.690002,128.360001,128.649994,121.806305,35626800 +2015-06-08,128.899994,129.210007,126.830002,127.800003,121.001518,52674800 +2015-06-09,126.699997,128.080002,125.620003,127.419998,120.641731,56075400 +2015-06-10,127.919998,129.339996,127.849998,128.880005,122.024086,39087300 +2015-06-11,129.179993,130.179993,128.479996,128.589996,121.749489,35390900 +2015-06-12,128.190002,128.330002,127.110001,127.169998,120.405037,36886200 +2015-06-15,126.099998,127.239998,125.709999,126.919998,120.168327,43988900 +2015-06-16,127.029999,127.849998,126.370003,127.599998,120.812149,31494100 +2015-06-17,127.720001,127.879997,126.739998,127.300003,120.528114,32918100 +2015-06-18,127.230003,128.309998,127.220001,127.879997,121.077255,35407200 +2015-06-19,127.709999,127.82,126.400002,126.599998,119.865356,54716900 +2015-06-22,127.489998,128.059998,127.080002,127.610001,120.821625,34039300 +2015-06-23,127.480003,127.610001,126.879997,127.029999,120.272484,30268900 +2015-06-24,127.209999,129.800003,127.120003,128.110001,121.295044,55280900 +2015-06-25,128.860001,129.199997,127.5,127.5,120.717476,31938100 +2015-06-26,127.669998,127.989998,126.510002,126.75,120.007362,44066800 +2015-06-29,125.459999,126.470001,124.480003,124.529999,117.905479,49161400 +2015-06-30,125.57,126.120003,124.860001,125.43,118.757591,44370700 +2015-07-01,126.900002,126.940002,125.989998,126.599998,119.865356,30238800 +2015-07-02,126.43,126.690002,125.769997,126.440002,119.713867,27211000 +2015-07-06,124.940002,126.230003,124.849998,126,119.297279,28060400 +2015-07-07,125.889999,126.150002,123.769997,125.690002,119.003761,46946800 +2015-07-08,124.480003,124.639999,122.540001,122.57,116.049744,60761600 +2015-07-09,123.849998,124.059998,119.220001,120.07,113.682739,77821600 +2015-07-10,121.940002,123.849998,121.209999,123.279999,116.721977,61354500 +2015-07-13,125.029999,125.760002,124.32,125.660004,118.975372,41440500 +2015-07-14,126.040001,126.370003,125.040001,125.610001,118.928009,31768100 +2015-07-15,125.720001,127.150002,125.580002,126.82,120.073647,33649200 +2015-07-16,127.739998,128.570007,127.349998,128.509995,121.673759,36222400 +2015-07-17,129.080002,129.619995,128.309998,129.619995,122.724678,46164700 +2015-07-20,130.970001,132.970001,130.699997,132.070007,125.044388,58900200 +2015-07-21,132.850006,132.919998,130.320007,130.75,123.794601,76756400 +2015-07-22,121.989998,125.5,121.989998,125.220001,118.558777,115450600 +2015-07-23,126.199997,127.089996,125.059998,125.160004,118.501961,50999500 +2015-07-24,125.32,125.739998,123.900002,124.5,117.877068,42162300 +2015-07-27,123.089996,123.610001,122.120003,122.769997,116.239098,44455500 +2015-07-28,123.379997,123.910004,122.550003,123.379997,116.816643,33618100 +2015-07-29,123.150002,123.5,122.269997,122.989998,116.447395,37011700 +2015-07-30,122.32,122.57,121.709999,122.370003,115.860397,33628300 +2015-07-31,122.599998,122.639999,120.910004,121.300003,114.847298,42885000 +2015-08-03,121.5,122.57,117.519997,118.440002,112.139442,69976000 +2015-08-04,117.419998,117.699997,113.25,114.639999,108.541588,124138600 +2015-08-05,112.949997,117.440002,112.099998,115.400002,109.261162,99312600 +2015-08-06,115.970001,116.5,114.120003,115.129997,109.498924,52903000 +2015-08-07,114.580002,116.25,114.5,115.519997,109.869843,38670400 +2015-08-10,116.529999,119.989998,116.529999,119.720001,113.864433,54951600 +2015-08-11,117.809998,118.18,113.330002,113.489998,107.93914,97082800 +2015-08-12,112.529999,115.419998,109.629997,115.239998,109.603546,101217500 +2015-08-13,116.040001,116.400002,114.540001,115.150002,109.517944,48535800 +2015-08-14,114.32,116.309998,114.010002,115.959999,110.288322,42929500 +2015-08-17,116.040001,117.650002,115.5,117.160004,111.429642,40884700 +2015-08-18,116.43,117.440002,116.010002,116.5,110.801933,34560700 +2015-08-19,116.099998,116.519997,114.68,115.010002,109.384789,47445700 +2015-08-20,114.080002,114.349998,111.629997,112.650002,107.140221,68501600 +2015-08-21,110.43,111.900002,105.650002,105.760002,100.587219,128275500 +2015-08-24,94.870003,108.800003,92,103.120003,98.076347,162206300 +2015-08-25,111.110001,111.110001,103.5,103.739998,98.666023,103601600 +2015-08-26,107.089996,109.889999,105.050003,109.690002,104.325012,96774600 +2015-08-27,112.230003,113.239998,110.019997,112.919998,107.397011,84616100 +2015-08-28,112.169998,113.309998,111.540001,113.290001,107.748924,53164400 +2015-08-31,112.029999,114.529999,112,112.760002,107.24485,56229300 +2015-09-01,110.150002,111.879997,107.360001,107.720001,102.451363,76845900 +2015-09-02,110.230003,112.339996,109.129997,112.339996,106.84539,61888800 +2015-09-03,112.489998,112.779999,110.040001,110.370003,104.971733,53233900 +2015-09-04,108.970001,110.449997,108.510002,109.269997,103.925537,49996300 +2015-09-08,111.75,112.559998,110.32,112.309998,106.816864,54843600 +2015-09-09,113.760002,114.019997,109.769997,110.150002,104.762497,85010800 +2015-09-10,110.269997,113.279999,109.900002,112.57,107.064133,62892800 +2015-09-11,111.790001,114.209999,111.760002,114.209999,108.623924,49915500 +2015-09-14,116.580002,116.889999,114.860001,115.309998,109.67012,58363400 +2015-09-15,115.93,116.529999,114.419998,116.279999,110.59269,43341200 +2015-09-16,116.25,116.540001,115.440002,116.410004,110.716324,37173500 +2015-09-17,115.660004,116.489998,113.720001,113.919998,108.348106,64112600 +2015-09-18,112.209999,114.300003,111.870003,113.449997,107.9011,74285300 +2015-09-21,113.669998,115.370003,113.660004,115.209999,109.575012,50222000 +2015-09-22,113.379997,114.18,112.519997,113.400002,107.853531,50346200 +2015-09-23,113.629997,114.720001,113.300003,114.32,108.728546,35756700 +2015-09-24,113.25,115.5,112.370003,115,109.375282,50219500 +2015-09-25,116.440002,116.690002,114.019997,114.709999,109.099472,56151900 +2015-09-28,113.849998,114.57,112.440002,112.440002,106.940506,52109000 +2015-09-29,112.830002,113.510002,107.860001,109.059998,103.7258,73365400 +2015-09-30,110.169998,111.540001,108.730003,110.300003,104.905174,66473000 +2015-10-01,109.07,109.620003,107.309998,109.580002,104.220398,63929100 +2015-10-02,108.010002,111.010002,107.550003,110.379997,104.981232,58019800 +2015-10-05,109.879997,111.370003,109.07,110.779999,105.361679,52064700 +2015-10-06,110.629997,111.739998,109.769997,111.309998,105.865768,48196800 +2015-10-07,111.739998,111.769997,109.410004,110.779999,105.361679,46765600 +2015-10-08,110.190002,110.190002,108.209999,109.5,104.144302,61979600 +2015-10-09,110,112.279999,109.489998,112.120003,106.636162,52766100 +2015-10-12,112.730003,112.75,111.440002,111.599998,106.141563,30467200 +2015-10-13,110.82,112.449997,110.68,111.790001,106.322281,33049300 +2015-10-14,111.290001,111.519997,109.559998,110.209999,104.819572,44462400 +2015-10-15,110.93,112.099998,110.489998,111.860001,106.38887,37673500 +2015-10-16,111.779999,112,110.529999,111.040001,105.608978,39232600 +2015-10-19,110.800003,111.75,110.110001,111.730003,106.265236,29759200 +2015-10-20,111.339996,114.169998,110.82,113.769997,108.205444,48778800 +2015-10-21,114,115.580002,113.699997,113.760002,108.195953,41795200 +2015-10-22,114.330002,115.5,114.099998,115.5,109.85083,41654100 +2015-10-23,116.699997,119.230003,116.330002,119.080002,113.255737,59366900 +2015-10-26,118.080002,118.129997,114.919998,115.279999,109.641594,66333800 +2015-10-27,115.400002,116.540001,113.989998,114.550003,108.947304,69884400 +2015-10-28,116.93,119.300003,116.059998,119.269997,113.436432,85551400 +2015-10-29,118.699997,120.690002,118.269997,120.529999,114.634811,51227300 +2015-10-30,120.989998,121.220001,119.449997,119.5,113.655197,49365300 +2015-11-02,120.800003,121.360001,119.610001,121.18,115.253029,32203300 +2015-11-03,120.790001,123.489998,120.699997,122.57,116.575035,45519000 +2015-11-04,123.129997,123.82,121.620003,122,116.032906,44886100 +2015-11-05,121.849998,122.690002,120.18,120.919998,115.498016,39552700 +2015-11-06,121.110001,121.809998,120.620003,121.059998,115.631737,33042300 +2015-11-09,120.959999,121.809998,120.050003,120.57,115.163704,33871400 +2015-11-10,116.900002,118.07,116.059998,116.769997,111.534096,59127900 +2015-11-11,116.370003,117.419998,115.209999,116.110001,110.903687,45218000 +2015-11-12,116.260002,116.82,115.650002,115.720001,110.531181,32525600 +2015-11-13,115.199997,115.57,112.269997,112.339996,107.30275,45812400 +2015-11-16,111.379997,114.239998,111,114.18,109.060242,38106700 +2015-11-17,114.919998,115.050003,113.32,113.690002,108.592209,27616900 +2015-11-18,115.760002,117.489998,115.5,117.290001,112.030785,46674700 +2015-11-19,117.639999,119.75,116.760002,118.779999,113.453979,43295800 +2015-11-20,119.199997,119.919998,118.849998,119.300003,113.950661,34287100 +2015-11-23,119.269997,119.730003,117.339996,117.75,112.470169,32482500 +2015-11-24,117.330002,119.349998,117.120003,118.879997,113.5495,42803200 +2015-11-25,119.209999,119.230003,117.919998,118.029999,112.737602,21388300 +2015-11-27,118.290001,118.410004,117.599998,117.809998,112.527473,13046400 +2015-11-30,117.989998,119.410004,117.75,118.300003,112.995499,39180300 +2015-12-01,118.75,118.809998,116.860001,117.339996,112.078529,34852400 +2015-12-02,117.339996,118.110001,116.080002,116.279999,111.066078,33386600 +2015-12-03,116.550003,116.790001,114.220001,115.199997,110.0345,41569500 +2015-12-04,115.290001,119.25,115.110001,119.029999,113.692772,57777000 +2015-12-07,118.980003,119.860001,117.809998,118.279999,112.976395,32084200 +2015-12-08,117.519997,118.599998,116.860001,118.230003,112.928642,34309500 +2015-12-09,117.639999,117.690002,115.080002,115.620003,110.435677,46361400 +2015-12-10,116.040001,116.940002,115.510002,116.169998,110.961006,29104200 +2015-12-11,115.190002,115.389999,112.849998,113.18,108.10508,46886200 +2015-12-14,112.18,112.68,109.790001,112.480003,107.436455,64318700 +2015-12-15,111.940002,112.800003,110.349998,110.489998,105.535706,53323100 +2015-12-16,111.07,111.989998,108.800003,111.339996,106.347572,56238500 +2015-12-17,112.019997,112.25,108.980003,108.980003,104.093407,44772800 +2015-12-18,108.910004,109.519997,105.809998,106.029999,101.275681,96453300 +2015-12-21,107.279999,107.370003,105.57,107.330002,102.51738,47590600 +2015-12-22,107.400002,107.720001,106.449997,107.230003,102.421867,32789400 +2015-12-23,107.269997,108.849998,107.199997,108.610001,103.740013,32657400 +2015-12-24,109,109,107.949997,108.029999,103.185997,13570400 +2015-12-28,107.589996,107.690002,106.18,106.82,102.030251,26704200 +2015-12-29,106.959999,109.43,106.860001,108.739998,103.864151,30931200 +2015-12-30,108.580002,108.699997,107.18,107.32,102.507828,25213800 +2015-12-31,107.010002,107.029999,104.82,105.260002,100.540207,40635300 +2016-01-04,102.610001,105.370003,102,105.349998,100.626175,67649400 +2016-01-05,105.75,105.849998,102.410004,102.709999,98.104546,55791000 +2016-01-06,100.559998,102.370003,99.870003,100.699997,96.184654,68457400 +2016-01-07,98.68,100.129997,96.43,96.449997,92.125244,81094400 +2016-01-08,98.550003,99.110001,96.760002,96.959999,92.612358,70798000 +2016-01-11,98.970001,99.059998,97.339996,98.529999,94.111984,49739400 +2016-01-12,100.550003,100.690002,98.839996,99.959999,95.477859,49154200 +2016-01-13,100.32,101.190002,97.300003,97.389999,93.023087,62439600 +2016-01-14,97.959999,100.480003,95.739998,99.519997,95.057579,63170100 +2016-01-15,96.199997,97.709999,95.360001,97.129997,92.77475,79010000 +2016-01-19,98.410004,98.650002,95.5,96.660004,92.325813,53087700 +2016-01-20,95.099998,98.190002,93.419998,96.790001,92.449989,72334400 +2016-01-21,97.059998,97.879997,94.940002,96.300003,91.981964,52161500 +2016-01-22,98.629997,101.459999,98.370003,101.419998,96.872398,65800500 +2016-01-25,101.519997,101.529999,99.209999,99.440002,94.981171,51794500 +2016-01-26,99.93,100.879997,98.07,99.989998,95.506508,75077000 +2016-01-27,96.040001,96.629997,93.339996,93.419998,89.231094,133369700 +2016-01-28,93.790001,94.519997,92.389999,94.089996,89.871048,55678800 +2016-01-29,94.790001,97.339996,94.349998,97.339996,92.975334,64416500 +2016-02-01,96.470001,96.709999,95.400002,96.43,92.10614,40943500 +2016-02-02,95.419998,96.040001,94.279999,94.480003,90.243576,37357200 +2016-02-03,95,96.839996,94.080002,96.349998,92.029724,45964300 +2016-02-04,95.860001,97.330002,95.190002,96.599998,92.76918,46471700 +2016-02-05,96.519997,96.919998,93.690002,94.019997,90.291489,46418100 +2016-02-08,93.129997,95.699997,93.040001,95.010002,91.242241,54021400 +2016-02-09,94.290001,95.940002,93.93,94.989998,91.223038,44331200 +2016-02-10,95.919998,96.349998,94.099998,94.269997,90.531593,42343600 +2016-02-11,93.790001,94.720001,92.589996,93.699997,89.984192,50074700 +2016-02-12,94.190002,94.5,93.010002,93.989998,90.262688,40351400 +2016-02-16,95.019997,96.849998,94.610001,96.639999,92.807602,49057900 +2016-02-17,96.669998,98.209999,96.150002,98.120003,94.228897,44863200 +2016-02-18,98.839996,98.889999,96.089996,96.260002,92.442665,39021000 +2016-02-19,96,96.760002,95.800003,96.040001,92.231384,35374200 +2016-02-22,96.309998,96.900002,95.919998,96.879997,93.038086,34280800 +2016-02-23,96.400002,96.5,94.550003,94.690002,90.934929,31942600 +2016-02-24,93.980003,96.379997,93.32,96.099998,92.289017,36255700 +2016-02-25,96.050003,96.760002,95.25,96.760002,92.922836,27582700 +2016-02-26,97.199997,98.019997,96.580002,96.910004,93.066895,28991100 +2016-02-29,96.860001,98.230003,96.650002,96.690002,92.855621,35216300 +2016-03-01,97.650002,100.769997,97.419998,100.529999,96.543343,50407100 +2016-03-02,100.510002,100.889999,99.639999,100.75,96.754623,33169600 +2016-03-03,100.580002,101.709999,100.449997,101.5,97.474876,36955700 +2016-03-04,102.370003,103.75,101.370003,103.010002,98.924988,46055100 +2016-03-07,102.389999,102.830002,100.959999,101.870003,97.8302,35828900 +2016-03-08,100.779999,101.760002,100.400002,101.029999,97.023506,31561900 +2016-03-09,101.309998,101.580002,100.269997,101.120003,97.109947,27201700 +2016-03-10,101.410004,102.239998,100.150002,101.169998,97.157944,33513600 +2016-03-11,102.239998,102.279999,101.5,102.260002,98.204727,27408200 +2016-03-14,101.910004,102.910004,101.779999,102.519997,98.454414,25076100 +2016-03-15,103.959999,105.18,103.849998,104.580002,100.432732,40067700 +2016-03-16,104.610001,106.309998,104.589996,105.970001,101.767609,38303500 +2016-03-17,105.519997,106.470001,104.959999,105.800003,101.60434,34420700 +2016-03-18,106.339996,106.5,105.190002,105.919998,101.719589,44205200 +2016-03-21,105.93,107.650002,105.139999,105.910004,101.709991,35502700 +2016-03-22,105.25,107.290001,105.209999,106.720001,102.487862,32444400 +2016-03-23,106.480003,107.07,105.900002,106.129997,101.921272,25703500 +2016-03-24,105.470001,106.25,104.889999,105.669998,101.479515,26133000 +2016-03-28,106,106.190002,105.059998,105.190002,101.018539,19411400 +2016-03-29,104.889999,107.790001,104.879997,107.68,103.409798,31190100 +2016-03-30,108.650002,110.419998,108.599998,109.559998,105.215225,45601100 +2016-03-31,109.720001,109.900002,108.879997,108.989998,104.667839,25888400 +2016-04-01,108.779999,110,108.199997,109.989998,105.628189,25874000 +2016-04-04,110.419998,112.190002,110.269997,111.120003,106.713379,37356200 +2016-04-05,109.510002,110.730003,109.419998,109.809998,105.455322,26578700 +2016-04-06,110.230003,110.980003,109.199997,110.959999,106.559723,26404100 +2016-04-07,109.949997,110.419998,108.120003,108.540001,104.235695,31801900 +2016-04-08,108.910004,109.769997,108.169998,108.660004,104.350922,23581700 +2016-04-11,108.970001,110.610001,108.830002,109.019997,104.69664,29407500 +2016-04-12,109.339996,110.5,108.660004,110.440002,106.060333,27232300 +2016-04-13,110.800003,112.339996,110.800003,112.040001,107.596893,33257300 +2016-04-14,111.620003,112.389999,111.330002,112.099998,107.65451,25473900 +2016-04-15,112.110001,112.300003,109.730003,109.849998,105.493736,46939000 +2016-04-18,108.889999,108.949997,106.940002,107.480003,103.217735,60834000 +2016-04-19,107.879997,108,106.230003,106.910004,102.670319,32384900 +2016-04-20,106.639999,108.089996,106.059998,107.129997,102.881599,30611000 +2016-04-21,106.93,106.93,105.519997,105.970001,101.767609,31552500 +2016-04-22,105.010002,106.480003,104.620003,105.68,101.489113,33683100 +2016-04-25,105,105.650002,104.510002,105.080002,100.912903,28031600 +2016-04-26,103.910004,105.300003,103.910004,104.349998,100.211845,56016200 +2016-04-27,96,98.709999,95.68,97.82,93.940796,114602100 +2016-04-28,97.610001,97.879997,94.25,94.830002,91.069382,82242700 +2016-04-29,93.989998,94.720001,92.510002,93.739998,90.022591,68531500 +2016-05-02,93.970001,94.080002,92.400002,93.639999,89.926582,48160100 +2016-05-03,94.199997,95.739998,93.68,95.18,91.405487,56831300 +2016-05-04,95.199997,95.900002,93.82,94.190002,90.454765,41025500 +2016-05-05,94,94.07,92.68,93.239998,90.087601,35890500 +2016-05-06,93.370003,93.449997,91.849998,92.720001,89.58519,43458200 +2016-05-09,93,93.769997,92.589996,92.790001,89.652824,32936400 +2016-05-10,93.330002,93.57,92.110001,93.419998,90.261513,33686800 +2016-05-11,93.480003,93.57,92.459999,92.510002,89.382286,28719100 +2016-05-12,92.720001,92.779999,89.470001,90.339996,87.285637,76314700 +2016-05-13,90,91.669998,90,90.519997,87.459572,44392800 +2016-05-16,92.389999,94.389999,91.650002,93.879997,90.705963,61259800 +2016-05-17,94.550003,94.699997,93.010002,93.489998,90.329147,46916900 +2016-05-18,94.160004,95.209999,93.889999,94.559998,91.362984,42062400 +2016-05-19,94.639999,94.639999,93.57,94.199997,91.015144,30442100 +2016-05-20,94.639999,95.43,94.519997,95.220001,92.000664,32026000 +2016-05-23,95.870003,97.190002,95.669998,96.43,93.169746,38018600 +2016-05-24,97.220001,98.089996,96.839996,97.900002,94.590057,35140200 +2016-05-25,98.669998,99.739998,98.110001,99.620003,96.251907,38168800 +2016-05-26,99.68,100.730003,98.639999,100.410004,97.01519,56331200 +2016-05-27,99.440002,100.470001,99.25,100.349998,96.957214,36229500 +2016-05-31,99.599998,100.400002,98.82,99.860001,96.48378,42307200 +2016-06-01,99.019997,99.540001,98.330002,98.459999,95.131126,29173300 +2016-06-02,97.599998,97.839996,96.629997,97.720001,94.416138,40191600 +2016-06-03,97.790001,98.269997,97.449997,97.919998,94.609375,28062900 +2016-06-06,97.989998,101.889999,97.550003,98.629997,95.295364,23292500 +2016-06-07,99.25,99.870003,98.959999,99.029999,95.681847,22409500 +2016-06-08,99.019997,99.559998,98.68,98.940002,95.594902,20848100 +2016-06-09,98.5,99.989998,98.459999,99.650002,96.280899,26601400 +2016-06-10,98.529999,99.349998,98.480003,98.830002,95.488617,31712900 +2016-06-13,98.690002,99.120003,97.099998,97.339996,94.048996,38020500 +2016-06-14,97.32,98.480003,96.75,97.459999,94.164932,31931900 +2016-06-15,97.82,98.410004,97.029999,97.139999,93.855766,29445200 +2016-06-16,96.449997,97.75,96.07,97.550003,94.251892,31326800 +2016-06-17,96.620003,96.650002,95.300003,95.330002,92.106941,61008200 +2016-06-20,96,96.57,95.029999,95.099998,91.88472,34411900 +2016-06-21,94.940002,96.349998,94.68,95.910004,92.667336,35546400 +2016-06-22,96.25,96.889999,95.349998,95.550003,92.319504,29219100 +2016-06-23,95.940002,96.290001,95.25,96.099998,92.850914,32240200 +2016-06-24,92.910004,94.660004,92.650002,93.400002,90.242188,75311400 +2016-06-27,93,93.050003,91.5,92.040001,88.928177,45489600 +2016-06-28,92.900002,93.660004,92.139999,93.589996,90.425781,40444900 +2016-06-29,93.970001,94.550003,93.629997,94.400002,91.208397,36531000 +2016-06-30,94.440002,95.769997,94.300003,95.599998,92.367805,35836400 +2016-07-01,95.489998,96.470001,95.330002,95.889999,92.64801,26026500 +2016-07-05,95.389999,95.400002,94.459999,94.989998,91.778442,27705200 +2016-07-06,94.599998,95.660004,94.370003,95.529999,92.300171,30949100 +2016-07-07,95.699997,96.5,95.620003,95.940002,92.696312,25139600 +2016-07-08,96.489998,96.889999,96.050003,96.68,93.411301,28912100 +2016-07-11,96.75,97.650002,96.730003,96.980003,93.701157,23794900 +2016-07-12,97.169998,97.699997,97.120003,97.419998,94.126282,24167500 +2016-07-13,97.410004,97.669998,96.839996,96.870003,93.594872,25892200 +2016-07-14,97.389999,98.989998,97.32,98.790001,95.449966,38919000 +2016-07-15,98.919998,99.300003,98.5,98.779999,95.4403,30137000 +2016-07-18,98.699997,100.129997,98.599998,99.830002,96.454803,36493900 +2016-07-19,99.559998,100,99.339996,99.870003,96.493454,23779900 +2016-07-20,100,100.459999,99.739998,99.959999,96.580414,26276000 +2016-07-21,99.830002,101,99.129997,99.43,96.068329,32702000 +2016-07-22,99.260002,99.300003,98.309998,98.660004,95.324364,28313700 +2016-07-25,98.25,98.839996,96.919998,97.339996,94.048996,40382900 +2016-07-26,96.82,97.970001,96.419998,96.669998,93.401634,56239800 +2016-07-27,104.269997,104.349998,102.75,102.949997,99.469307,92344800 +2016-07-28,102.830002,104.449997,102.82,104.339996,100.812317,39869800 +2016-07-29,104.190002,104.550003,103.68,104.209999,100.686714,27733700 +2016-08-01,104.410004,106.150002,104.410004,106.050003,102.464508,38167900 +2016-08-02,106.050003,106.07,104,104.480003,100.947594,33816600 +2016-08-03,104.809998,105.839996,104.769997,105.790001,102.213295,30202600 +2016-08-04,105.580002,106,105.279999,105.870003,102.844727,27408700 +2016-08-05,106.269997,107.650002,106.18,107.480003,104.408714,40553400 +2016-08-08,107.519997,108.370003,107.160004,108.370003,105.273285,28037200 +2016-08-09,108.230003,108.940002,108.010002,108.809998,105.700714,26315200 +2016-08-10,108.709999,108.900002,107.760002,108,104.913849,24008500 +2016-08-11,108.519997,108.93,107.849998,107.93,104.845863,27484500 +2016-08-12,107.779999,108.440002,107.779999,108.18,105.088715,18660400 +2016-08-15,108.139999,109.540001,108.080002,109.480003,106.35157,25868200 +2016-08-16,109.629997,110.230003,109.209999,109.379997,106.254425,33794400 +2016-08-17,109.099998,109.370003,108.339996,109.220001,106.098991,25356000 +2016-08-18,109.230003,109.599998,109.019997,109.080002,105.962997,21984700 +2016-08-19,108.769997,109.690002,108.360001,109.360001,106.234985,25368100 +2016-08-22,108.860001,109.099998,107.849998,108.510002,105.409286,25820200 +2016-08-23,108.589996,109.32,108.529999,108.849998,105.739555,21257700 +2016-08-24,108.57,108.75,107.68,108.029999,104.943001,23675100 +2016-08-25,107.389999,107.879997,106.68,107.57,104.496147,25086200 +2016-08-26,107.410004,107.949997,106.309998,106.940002,103.88414,27766300 +2016-08-29,106.620003,107.440002,106.290001,106.82,103.767578,24970300 +2016-08-30,105.800003,106.5,105.5,106,102.971016,24863900 +2016-08-31,105.660004,106.57,105.639999,106.099998,103.068153,29662400 +2016-09-01,106.139999,106.800003,105.620003,106.730003,103.680153,26701500 +2016-09-02,107.699997,108,106.82,107.730003,104.651573,26802500 +2016-09-06,107.900002,108.300003,107.510002,107.699997,104.622429,26880400 +2016-09-07,107.830002,108.760002,107.07,108.360001,105.263565,42364300 +2016-09-08,107.25,107.269997,105.239998,105.519997,102.504723,53002000 +2016-09-09,104.639999,105.720001,103.129997,103.129997,100.183022,46557000 +2016-09-12,102.650002,105.720001,102.529999,105.440002,102.42701,45292800 +2016-09-13,107.510002,108.790001,107.239998,107.949997,104.865273,62176200 +2016-09-14,108.730003,113.029999,108.599998,111.769997,108.576126,110888700 +2016-09-15,113.860001,115.730003,113.489998,115.57,112.26754,89983600 +2016-09-16,115.120003,116.129997,114.040001,114.919998,111.636124,79886900 +2016-09-19,115.190002,116.18,113.25,113.580002,110.334419,47023000 +2016-09-20,113.050003,114.120003,112.510002,113.57,110.324692,34514300 +2016-09-21,113.849998,113.989998,112.440002,113.550003,110.305275,36003200 +2016-09-22,114.349998,114.940002,114,114.620003,111.344696,31074000 +2016-09-23,114.419998,114.790001,111.550003,112.709999,109.489258,52481200 +2016-09-26,111.639999,113.389999,111.550003,112.879997,109.654411,29869400 +2016-09-27,113,113.18,112.339996,113.089996,109.858406,24607400 +2016-09-28,113.690002,114.639999,113.43,113.949997,110.69384,29641100 +2016-09-29,113.160004,113.800003,111.800003,112.18,108.974403,35887000 +2016-09-30,112.459999,113.370003,111.800003,113.050003,109.81955,36379100 +2016-10-03,112.709999,113.050003,112.279999,112.519997,109.304688,21701800 +2016-10-04,113.059998,114.309998,112.629997,113,109.770973,29736800 +2016-10-05,113.400002,113.660004,112.690002,113.050003,109.81955,21453100 +2016-10-06,113.699997,114.339996,113.129997,113.889999,110.635551,28779300 +2016-10-07,114.309998,114.559998,113.510002,114.059998,110.800682,24358400 +2016-10-10,115.019997,116.75,114.720001,116.050003,112.733818,36236000 +2016-10-11,117.699997,118.690002,116.199997,116.300003,112.976685,64041000 +2016-10-12,117.349998,117.980003,116.75,117.339996,113.986969,37586800 +2016-10-13,116.790001,117.440002,115.720001,116.980003,113.637253,35192400 +2016-10-14,117.879997,118.169998,117.129997,117.629997,114.268661,35652200 +2016-10-17,117.330002,117.839996,116.779999,117.550003,114.190964,23624900 +2016-10-18,118.18,118.209999,117.449997,117.470001,114.113251,24553500 +2016-10-19,117.25,117.760002,113.800003,117.120003,113.773247,20034600 +2016-10-20,116.860001,117.379997,116.330002,117.059998,113.714958,24125800 +2016-10-21,116.809998,116.910004,116.279999,116.599998,113.268097,23192700 +2016-10-24,117.099998,117.739998,117,117.650002,114.288109,23538700 +2016-10-25,117.949997,118.360001,117.309998,118.25,114.870956,48129000 +2016-10-26,114.309998,115.699997,113.309998,115.589996,112.286957,66134200 +2016-10-27,115.389999,115.860001,114.099998,114.480003,111.208687,34562000 +2016-10-28,113.870003,115.209999,113.449997,113.720001,110.470398,37861700 +2016-10-31,113.650002,114.230003,113.199997,113.540001,110.29554,26419400 +2016-11-01,113.459999,113.769997,110.529999,111.489998,108.304131,43825800 +2016-11-02,111.400002,112.349998,111.230003,111.589996,108.401276,28331700 +2016-11-03,110.980003,111.459999,109.550003,109.830002,107.239349,26932600 +2016-11-04,108.529999,110.25,108.110001,108.839996,106.272697,30837000 +2016-11-07,110.080002,110.510002,109.459999,110.410004,107.805664,32560000 +2016-11-08,110.309998,111.720001,109.699997,111.059998,108.440315,24054500 +2016-11-09,109.879997,111.32,108.050003,110.879997,108.264572,59176400 +2016-11-10,111.089996,111.089996,105.830002,107.790001,105.247467,57134500 +2016-11-11,107.120003,108.870003,106.550003,108.43,105.872368,34094100 +2016-11-14,107.709999,107.809998,104.080002,105.709999,103.216522,51175500 +2016-11-15,106.57,107.68,106.160004,107.110001,104.583504,32264500 +2016-11-16,106.699997,110.230003,106.599998,109.989998,107.395561,58840500 +2016-11-17,109.809998,110.349998,108.830002,109.949997,107.356506,27632000 +2016-11-18,109.720001,110.540001,109.660004,110.059998,107.463921,28428900 +2016-11-21,110.120003,111.989998,110.010002,111.730003,109.094528,29264600 +2016-11-22,111.949997,112.419998,111.400002,111.800003,109.162872,25965500 +2016-11-23,111.360001,111.510002,110.330002,111.230003,108.606323,27426400 +2016-11-25,111.129997,111.870003,110.949997,111.790001,109.153107,11475900 +2016-11-28,111.43,112.470001,111.389999,111.57,108.938301,27194000 +2016-11-29,110.779999,112.029999,110.07,111.459999,108.830894,28528800 +2016-11-30,111.599998,112.199997,110.269997,110.519997,107.913071,36162300 +2016-12-01,110.370003,110.940002,109.029999,109.489998,106.907356,37086900 +2016-12-02,109.169998,110.089996,108.849998,109.900002,107.307686,26528000 +2016-12-05,110,110.029999,108.25,109.110001,106.536316,34324500 +2016-12-06,109.5,110.360001,109.190002,109.949997,107.356506,26195500 +2016-12-07,109.260002,111.190002,109.160004,111.029999,108.411034,29998700 +2016-12-08,110.860001,112.43,110.599998,112.120003,109.475334,27068300 +2016-12-09,112.309998,114.699997,112.309998,113.949997,111.262154,34402600 +2016-12-12,113.290001,115,112.489998,113.300003,110.627495,26374400 +2016-12-13,113.839996,115.919998,113.75,115.190002,112.472916,43733800 +2016-12-14,115.040001,116.199997,114.980003,115.190002,112.472916,34031800 +2016-12-15,115.379997,116.730003,115.230003,115.82,113.088043,46524500 +2016-12-16,116.470001,116.5,115.650002,115.970001,113.234512,44351100 +2016-12-19,115.800003,117.379997,115.75,116.639999,113.88871,27779400 +2016-12-20,116.739998,117.5,116.68,116.949997,114.191406,21425000 +2016-12-21,116.800003,117.400002,116.779999,117.059998,114.298805,23783200 +2016-12-22,116.349998,116.510002,115.639999,116.290001,113.546967,26085900 +2016-12-23,115.589996,116.519997,115.589996,116.519997,113.77153,14181200 +2016-12-27,116.519997,117.800003,116.489998,117.260002,114.494087,18296900 +2016-12-28,117.519997,118.019997,116.199997,116.760002,114.005882,20905900 +2016-12-29,116.449997,117.110001,116.400002,116.730003,113.976585,15039500 +2016-12-30,116.650002,117.199997,115.43,115.82,113.088043,30586300 +2017-01-03,115.800003,116.330002,114.760002,116.150002,113.410263,28781900 +2017-01-04,115.849998,116.510002,115.75,116.019997,113.283333,21118100 +2017-01-05,115.919998,116.860001,115.809998,116.610001,113.859421,22193600 +2017-01-06,116.779999,118.160004,116.470001,117.910004,115.128761,31751900 +2017-01-09,117.949997,119.43,117.940002,118.989998,116.183273,33561900 +2017-01-10,118.769997,119.379997,118.300003,119.110001,116.300438,24462100 +2017-01-11,118.739998,119.93,118.599998,119.75,116.925346,27588600 +2017-01-12,118.900002,119.300003,118.209999,119.25,116.437141,27086200 +2017-01-13,119.110001,119.620003,118.809998,119.040001,116.232101,26111900 +2017-01-17,118.339996,120.239998,118.220001,120,117.169449,34439800 +2017-01-18,120,120.5,119.709999,119.989998,117.159683,23713000 +2017-01-19,119.400002,120.089996,119.370003,119.779999,116.954636,25597300 +2017-01-20,120.449997,120.449997,119.730003,120,117.169449,32597900 +2017-01-23,120,120.809998,119.769997,120.080002,117.247566,22050200 +2017-01-24,119.550003,120.099998,119.5,119.970001,117.14016,23211000 +2017-01-25,120.419998,122.099998,120.279999,121.879997,119.005104,32377600 +2017-01-26,121.669998,122.440002,121.599998,121.940002,119.063698,26337600 +2017-01-27,122.139999,122.349998,121.599998,121.949997,119.073456,20562900 +2017-01-30,120.93,121.629997,120.660004,121.629997,118.761009,30377500 +2017-01-31,121.150002,121.389999,120.620003,121.349998,118.487602,49201000 +2017-02-01,127.029999,130.490005,127.010002,128.75,125.713051,111985000 +2017-02-02,127.980003,129.389999,127.779999,128.529999,125.498253,33710400 +2017-02-03,128.309998,129.190002,128.160004,129.080002,126.035263,24507300 +2017-02-06,129.130005,130.5,128.899994,130.289993,127.216721,26845900 +2017-02-07,130.539993,132.089996,130.449997,131.529999,128.427475,38183800 +2017-02-08,131.350006,132.220001,131.220001,132.039993,128.925446,23004100 +2017-02-09,131.649994,132.449997,131.119995,132.419998,129.857056,28349900 +2017-02-10,132.460007,132.940002,132.050003,132.119995,129.562866,20065500 +2017-02-13,133.080002,133.820007,132.75,133.289993,130.71019,23035400 +2017-02-14,133.470001,135.089996,133.25,135.020004,132.406754,33226200 +2017-02-15,135.520004,136.270004,134.619995,135.509995,132.887253,35623100 +2017-02-16,135.669998,135.899994,134.839996,135.350006,132.730377,22584600 +2017-02-17,135.100006,135.830002,135.100006,135.720001,133.093216,22198200 +2017-02-21,136.229996,136.75,135.979996,136.699997,134.054214,24507200 +2017-02-22,136.429993,137.119995,136.110001,137.110001,134.456299,20836900 +2017-02-23,137.380005,137.479996,136.300003,136.529999,133.887512,20788200 +2017-02-24,135.910004,136.660004,135.279999,136.660004,134.015015,21776600 +2017-02-27,137.139999,137.440002,136.279999,136.929993,134.279785,20257400 +2017-02-28,137.080002,137.440002,136.699997,136.990005,134.338623,23482900 +2017-03-01,137.889999,140.149994,137.600006,139.789993,137.084412,36414600 +2017-03-02,140,140.279999,138.759995,138.960007,136.270508,26211000 +2017-03-03,138.779999,139.830002,138.589996,139.779999,137.074615,21108100 +2017-03-06,139.369995,139.770004,138.600006,139.339996,136.643127,21750000 +2017-03-07,139.059998,139.979996,138.789993,139.520004,136.819656,17446300 +2017-03-08,138.949997,139.800003,138.820007,139,136.309723,18707200 +2017-03-09,138.740005,138.789993,137.050003,138.679993,135.995895,22155900 +2017-03-10,139.25,139.360001,138.639999,139.139999,136.447006,19612800 +2017-03-13,138.850006,139.429993,138.820007,139.199997,136.505829,17421700 +2017-03-14,139.300003,139.649994,138.839996,138.990005,136.299911,15309100 +2017-03-15,139.410004,140.75,139.029999,140.460007,137.74147,25691800 +2017-03-16,140.720001,141.020004,140.259995,140.690002,137.96698,19232000 +2017-03-17,141,141,139.889999,139.990005,137.280563,43885000 +2017-03-20,140.399994,141.5,140.229996,141.460007,138.722122,21542000 +2017-03-21,142.110001,142.800003,139.729996,139.839996,137.133453,39529900 +2017-03-22,139.850006,141.600006,139.759995,141.419998,138.682877,25860200 +2017-03-23,141.259995,141.580002,140.610001,140.919998,138.192551,20346300 +2017-03-24,141.5,141.740005,140.350006,140.639999,137.917984,22395600 +2017-03-27,139.389999,141.220001,138.619995,140.880005,138.153336,23575100 +2017-03-28,140.910004,144.039993,140.619995,143.800003,141.016815,33374800 +2017-03-29,143.679993,144.490005,143.190002,144.119995,141.330612,29190000 +2017-03-30,144.190002,144.5,143.5,143.929993,141.144287,21207300 +2017-03-31,143.720001,144.270004,143.009995,143.660004,140.879517,19661700 +2017-04-03,143.710007,144.119995,143.050003,143.699997,140.918762,19985700 +2017-04-04,143.25,144.889999,143.169998,144.770004,141.968033,19891400 +2017-04-05,144.220001,145.460007,143.809998,144.020004,141.232559,27717900 +2017-04-06,144.289993,144.520004,143.449997,143.660004,140.879517,21149000 +2017-04-07,143.729996,144.179993,143.270004,143.339996,140.565704,16672200 +2017-04-10,143.600006,143.880005,142.899994,143.169998,140.399017,18933400 +2017-04-11,142.940002,143.350006,140.059998,141.630005,138.888809,30379400 +2017-04-12,141.600006,142.149994,141.009995,141.800003,139.055542,20350000 +2017-04-13,141.910004,142.380005,141.050003,141.050003,138.320038,17822900 +2017-04-17,141.479996,141.880005,140.869995,141.830002,139.084946,16582100 +2017-04-18,141.410004,142.039993,141.110001,141.199997,138.467133,14697500 +2017-04-19,141.880005,142,140.449997,140.679993,137.957184,17328400 +2017-04-20,141.220001,142.919998,141.160004,142.440002,139.683151,23319600 +2017-04-21,142.440002,142.679993,141.850006,142.270004,139.516418,17320900 +2017-04-24,143.5,143.949997,143.179993,143.639999,140.859909,17134300 +2017-04-25,143.910004,144.899994,143.869995,144.529999,141.732681,18871500 +2017-04-26,144.470001,144.600006,143.380005,143.679993,140.899139,20041200 +2017-04-27,143.919998,144.160004,143.309998,143.789993,141.007004,14246300 +2017-04-28,144.089996,144.300003,143.270004,143.649994,140.869705,20860400 +2017-05-01,145.100006,147.199997,144.960007,146.580002,143.743011,33602900 +2017-05-02,147.539993,148.089996,146.839996,147.509995,144.654999,45352200 +2017-05-03,145.589996,147.490005,144.270004,147.059998,144.213715,45697000 +2017-05-04,146.520004,147.139999,145.809998,146.529999,143.693954,23371900 +2017-05-05,146.759995,148.979996,146.759995,148.960007,146.076935,27327700 +2017-05-08,149.029999,153.699997,149.029999,153.009995,150.048538,48752400 +2017-05-09,153.869995,154.880005,153.449997,153.990005,151.009598,39130400 +2017-05-10,153.630005,153.940002,152.110001,153.259995,150.293716,25805700 +2017-05-11,152.449997,154.070007,152.309998,153.949997,151.593491,27255100 +2017-05-12,154.699997,156.419998,154.669998,156.100006,153.710602,32527000 +2017-05-15,156.009995,156.649994,155.050003,155.699997,153.316727,26009700 +2017-05-16,155.940002,156.059998,154.720001,155.470001,153.090256,20048500 +2017-05-17,153.600006,154.570007,149.710007,150.25,147.95015,50767700 +2017-05-18,151.270004,153.339996,151.130005,152.539993,150.205078,33568200 +2017-05-19,153.380005,153.979996,152.630005,153.059998,150.717133,26960800 +2017-05-22,154,154.580002,152.910004,153.990005,151.632904,22966400 +2017-05-23,154.899994,154.899994,153.309998,153.800003,151.445801,19918900 +2017-05-24,153.839996,154.169998,152.669998,153.339996,150.992844,19178000 +2017-05-25,153.729996,154.350006,153.029999,153.869995,151.514725,19235600 +2017-05-26,154,154.240005,153.309998,153.610001,151.258728,21701100 +2017-05-30,153.419998,154.429993,153.330002,153.669998,151.317795,20126900 +2017-05-31,153.970001,154.169998,152.380005,152.759995,150.421722,24451200 +2017-06-01,153.169998,153.330002,152.220001,153.179993,150.835281,16404100 +2017-06-02,153.580002,155.449997,152.889999,155.449997,153.070541,27770700 +2017-06-05,154.339996,154.449997,153.460007,153.929993,151.573807,25331700 +2017-06-06,153.899994,155.809998,153.779999,154.449997,152.085846,26624900 +2017-06-07,155.020004,155.979996,154.479996,155.369995,152.991776,21069600 +2017-06-08,155.25,155.539993,154.399994,154.990005,152.617584,21250800 +2017-06-09,155.190002,155.190002,146.020004,148.979996,146.69957,64882700 +2017-06-12,145.740005,146.089996,142.509995,145.419998,143.194092,72307300 +2017-06-13,147.160004,147.449997,145.149994,146.589996,144.346161,34165400 +2017-06-14,147.5,147.5,143.839996,145.160004,142.938065,31531200 +2017-06-15,143.320007,144.479996,142.210007,144.289993,142.08136,32165400 +2017-06-16,143.779999,144.5,142.199997,142.270004,140.0923,50361100 +2017-06-19,143.660004,146.740005,143.660004,146.339996,144.099991,32541400 +2017-06-20,146.869995,146.869995,144.940002,145.009995,142.790359,24900100 +2017-06-21,145.520004,146.070007,144.610001,145.869995,143.637177,21265800 +2017-06-22,145.770004,146.699997,145.119995,145.630005,143.400879,19106300 +2017-06-23,145.130005,147.160004,145.110001,146.279999,144.040924,35439400 +2017-06-26,147.169998,148.279999,145.380005,145.820007,143.587967,25692400 +2017-06-27,145.009995,146.160004,143.619995,143.729996,141.529938,24761900 +2017-06-28,144.490005,146.110001,143.160004,145.830002,143.597794,22082400 +2017-06-29,144.710007,145.130005,142.279999,143.679993,141.480713,31499400 +2017-06-30,144.449997,144.960007,143.779999,144.020004,141.815506,23024100 +2017-07-03,144.880005,145.300003,143.100006,143.5,141.303467,14258300 +2017-07-05,143.690002,144.789993,142.720001,144.089996,141.88443,21569600 +2017-07-06,143.020004,143.5,142.410004,142.729996,140.545258,24128800 +2017-07-07,142.899994,144.75,142.899994,144.179993,141.973053,19201700 +2017-07-10,144.110001,145.949997,143.369995,145.059998,142.839584,21090600 +2017-07-11,144.729996,145.850006,144.380005,145.529999,143.302399,19781800 +2017-07-12,145.869995,146.179993,144.820007,145.740005,143.509186,24884500 +2017-07-13,145.5,148.490005,145.440002,147.770004,145.508102,25199400 +2017-07-14,147.970001,149.330002,147.330002,149.039993,146.758636,20132100 +2017-07-17,148.820007,150.899994,148.570007,149.559998,147.270706,23793500 +2017-07-18,149.199997,150.130005,148.669998,150.080002,147.782761,17868800 +2017-07-19,150.479996,151.419998,149.949997,151.020004,148.708359,20923000 +2017-07-20,151.5,151.740005,150.190002,150.339996,148.038757,17243700 +2017-07-21,149.990005,150.440002,148.880005,150.270004,147.969849,26252600 +2017-07-24,150.580002,152.440002,149.899994,152.089996,149.761978,21493200 +2017-07-25,151.800003,153.839996,151.800003,152.740005,150.402039,18853900 +2017-07-26,153.350006,153.929993,153.059998,153.460007,151.111023,15781000 +2017-07-27,153.75,153.990005,147.300003,150.559998,148.255402,32476300 +2017-07-28,149.889999,150.229996,149.190002,149.5,147.211624,17213700 +2017-07-31,149.899994,150.330002,148.130005,148.729996,146.4534,19845900 +2017-08-01,149.100006,150.220001,148.410004,150.050003,147.753204,35368600 +2017-08-02,159.279999,159.75,156.160004,157.139999,154.734665,69936800 +2017-08-03,157.050003,157.210007,155.020004,155.570007,153.188705,27097300 +2017-08-04,156.070007,157.399994,155.690002,156.389999,153.99614,20559900 +2017-08-07,157.059998,158.919998,156.669998,158.809998,156.37912,21870300 +2017-08-08,158.600006,161.830002,158.270004,160.080002,157.629684,36205900 +2017-08-09,159.259995,161.270004,159.110001,161.059998,158.594666,26131500 +2017-08-10,159.899994,160,154.630005,155.320007,153.543152,40804300 +2017-08-11,156.600006,158.570007,156.070007,157.479996,155.678436,26257100 +2017-08-14,159.320007,160.210007,158.75,159.850006,158.021317,22122700 +2017-08-15,160.660004,162.199997,160.139999,161.600006,159.751297,29465500 +2017-08-16,161.940002,162.509995,160.149994,160.949997,159.108719,27671600 +2017-08-17,160.520004,160.710007,157.839996,157.860001,156.054092,27940600 +2017-08-18,157.860001,159.5,156.720001,157.5,155.698212,27428100 +2017-08-21,157.5,157.889999,155.110001,157.210007,155.41153,26368500 +2017-08-22,158.229996,160,158.020004,159.779999,157.952118,21604600 +2017-08-23,159.070007,160.470001,158.880005,159.979996,158.149826,19399100 +2017-08-24,160.429993,160.740005,158.550003,159.270004,157.447952,19818900 +2017-08-25,159.649994,160.559998,159.270004,159.860001,158.031189,25480100 +2017-08-28,160.139999,162,159.929993,161.470001,159.622787,25966000 +2017-08-29,160.100006,163.119995,160,162.910004,161.046326,29516900 +2017-08-30,163.800003,163.889999,162.610001,163.350006,161.481277,27269600 +2017-08-31,163.639999,164.520004,163.479996,164,162.12384,26785100 +2017-09-01,164.800003,164.940002,163.630005,164.050003,162.173279,16591100 +2017-09-05,163.75,164.25,160.559998,162.080002,160.225815,29468500 +2017-09-06,162.710007,162.990005,160.520004,161.910004,160.057755,21651700 +2017-09-07,162.089996,162.240005,160.360001,161.259995,159.415192,21928500 +2017-09-08,160.860001,161.149994,158.529999,158.630005,156.815277,28611500 +2017-09-11,160.5,162.050003,159.889999,161.5,159.652451,31085900 +2017-09-12,162.610001,163.960007,158.770004,160.860001,159.01976,71714000 +2017-09-13,159.869995,159.960007,157.910004,159.649994,157.823593,44907400 +2017-09-14,158.990005,159.399994,158.089996,158.279999,156.469269,23760700 +2017-09-15,158.470001,160.970001,158,159.880005,158.050964,49114600 +2017-09-18,160.110001,160.5,158,158.669998,156.854813,28269400 +2017-09-19,159.509995,159.770004,158.440002,158.729996,156.914124,20810600 +2017-09-20,157.899994,158.259995,153.830002,156.070007,154.284561,52951400 +2017-09-21,155.800003,155.800003,152.75,153.389999,151.635208,37511700 +2017-09-22,151.539993,152.270004,150.559998,151.889999,150.15239,46645400 +2017-09-25,149.990005,151.830002,149.160004,150.550003,148.827698,44387300 +2017-09-26,151.779999,153.919998,151.690002,153.139999,151.388062,36660000 +2017-09-27,153.800003,154.720001,153.539993,154.229996,152.465607,25504200 +2017-09-28,153.889999,154.279999,152.699997,153.279999,151.526474,22005500 +2017-09-29,153.210007,154.130005,152,154.119995,152.356857,26299800 +2017-10-02,154.259995,154.449997,152.720001,153.809998,152.050415,18698800 +2017-10-03,154.009995,155.089996,153.910004,154.479996,152.712738,16230300 +2017-10-04,153.630005,153.860001,152.460007,153.479996,151.724197,20163800 +2017-10-05,154.179993,155.440002,154.050003,155.389999,153.612335,21283800 +2017-10-06,154.970001,155.490005,154.559998,155.300003,153.523361,17407600 +2017-10-09,155.809998,156.729996,155.490005,155.839996,154.057175,16262900 +2017-10-10,156.059998,158,155.100006,155.899994,154.116486,15617000 +2017-10-11,155.970001,156.979996,155.75,156.550003,154.759079,16905600 +2017-10-12,156.350006,157.369995,155.729996,156,154.215363,16125100 +2017-10-13,156.729996,157.279999,156.410004,156.990005,155.194031,16394200 +2017-10-16,157.899994,160,157.649994,159.880005,158.050964,24121500 +2017-10-17,159.779999,160.869995,159.229996,160.470001,158.634216,18997300 +2017-10-18,160.419998,160.710007,159.600006,159.759995,157.932343,16374200 +2017-10-19,156.75,157.080002,155.020004,155.979996,154.195572,42584200 +2017-10-20,156.610001,157.75,155.960007,156.25,154.462494,23974100 +2017-10-23,156.889999,157.690002,155.5,156.169998,154.383408,21984300 +2017-10-24,156.289993,157.419998,156.199997,157.100006,155.302765,17757200 +2017-10-25,156.910004,157.550003,155.270004,156.410004,154.620682,21207100 +2017-10-26,157.229996,157.830002,156.779999,157.410004,155.609222,17000500 +2017-10-27,159.289993,163.600006,158.699997,163.050003,161.184708,44454200 +2017-10-30,163.889999,168.070007,163.720001,166.720001,164.812729,44700800 +2017-10-31,167.899994,169.649994,166.940002,169.039993,167.106186,36046800 +2017-11-01,169.869995,169.940002,165.610001,166.889999,164.980774,33637800 +2017-11-02,166.600006,168.5,165.279999,168.110001,166.186829,41393400 +2017-11-03,174,174.259995,171.119995,172.5,170.526596,59398600 +2017-11-06,172.369995,174.990005,171.720001,174.25,172.256577,35026300 +2017-11-07,173.910004,175.25,173.600006,174.809998,172.810165,24361500 +2017-11-08,174.660004,176.240005,174.330002,176.240005,174.223831,24409500 +2017-11-09,175.110001,176.100006,173.139999,175.880005,173.86795,29482600 +2017-11-10,175.110001,175.380005,174.270004,174.669998,173.292511,25145500 +2017-11-13,173.5,174.5,173.399994,173.970001,172.598022,16982100 +2017-11-14,173.039993,173.479996,171.179993,171.339996,169.988754,24782500 +2017-11-15,169.970001,170.320007,168.380005,169.080002,167.746582,29158100 +2017-11-16,171.179993,171.869995,170.300003,171.100006,169.750671,23637500 +2017-11-17,171.039993,171.389999,169.639999,170.149994,168.808151,21899500 +2017-11-20,170.289993,170.559998,169.559998,169.979996,168.639496,16262400 +2017-11-21,170.779999,173.699997,170.779999,173.139999,171.774567,25131300 +2017-11-22,173.360001,175,173.050003,174.960007,173.580231,25588900 +2017-11-24,175.100006,175.5,174.649994,174.970001,173.590149,14026700 +2017-11-27,175.050003,175.080002,173.339996,174.089996,172.717072,20716800 +2017-11-28,174.300003,174.869995,171.860001,173.070007,171.705139,26428800 +2017-11-29,172.630005,172.919998,167.160004,169.479996,168.143433,41666400 +2017-11-30,170.429993,172.139999,168.440002,171.850006,170.494751,41527200 +2017-12-01,169.949997,171.669998,168.5,171.050003,169.70105,39759300 +2017-12-04,172.479996,172.619995,169.630005,169.800003,168.460922,32542400 +2017-12-05,169.059998,171.520004,168.399994,169.639999,168.30217,27350200 +2017-12-06,167.5,170.199997,166.460007,169.009995,167.677139,28560000 +2017-12-07,169.029999,170.440002,168.910004,169.320007,167.984711,25673300 +2017-12-08,170.490005,171,168.820007,169.369995,168.034302,23355200 +2017-12-11,169.199997,172.889999,168.789993,172.669998,171.308289,35273800 +2017-12-12,172.149994,172.389999,171.460007,171.699997,170.345932,19409200 +2017-12-13,172.5,173.539993,172,172.270004,170.911438,23818400 +2017-12-14,172.399994,173.130005,171.649994,172.220001,170.861832,20476500 +2017-12-15,173.630005,174.169998,172.460007,173.970001,172.598022,40169300 +2017-12-18,174.880005,177.199997,174.860001,176.419998,175.028717,29421100 +2017-12-19,175.029999,175.389999,174.089996,174.539993,173.163528,27436400 +2017-12-20,174.869995,175.419998,173.25,174.350006,172.975037,23475600 +2017-12-21,174.169998,176.020004,174.100006,175.009995,173.629822,20949900 +2017-12-22,174.679993,175.419998,174.5,175.009995,173.629822,16349400 +2017-12-26,170.800003,171.470001,169.679993,170.570007,169.224838,33185500 +2017-12-27,170.100006,170.779999,169.710007,170.600006,169.254608,21498200 +2017-12-28,171,171.850006,170.479996,171.080002,169.73082,16480200 +2017-12-29,170.520004,170.589996,169.220001,169.229996,167.895416,25999900 +2018-01-02,170.160004,172.300003,169.259995,172.259995,170.901505,25555900 +2018-01-03,172.529999,174.550003,171.960007,172.229996,170.871735,29517900 +2018-01-04,172.539993,173.470001,172.080002,173.029999,171.665436,22434600 +2018-01-05,173.440002,175.369995,173.050003,175,173.619904,23660000 +2018-01-08,174.350006,175.610001,173.929993,174.350006,172.975037,20567800 +2018-01-09,174.550003,175.059998,173.410004,174.330002,172.9552,21584000 +2018-01-10,173.160004,174.300003,173,174.289993,172.915497,23959900 +2018-01-11,174.589996,175.490005,174.490005,175.279999,173.897705,18667700 +2018-01-12,176.179993,177.360001,175.649994,177.089996,175.69342,25418100 +2018-01-16,177.899994,179.389999,176.139999,176.190002,174.800537,29565900 +2018-01-17,176.149994,179.25,175.070007,179.100006,177.687576,34386800 +2018-01-18,179.369995,180.100006,178.25,179.259995,177.846313,31193400 +2018-01-19,178.610001,179.580002,177.410004,178.460007,177.052628,32425100 +2018-01-22,177.300003,177.779999,176.600006,177,175.604141,27108600 +2018-01-23,177.300003,179.440002,176.820007,177.039993,175.643814,32689100 +2018-01-24,177.25,177.300003,173.199997,174.220001,172.846054,51105100 +2018-01-25,174.509995,174.949997,170.529999,171.110001,169.760574,41529000 +2018-01-26,172,172,170.059998,171.509995,170.157425,39143000 +2018-01-29,170.160004,170.160004,167.070007,167.960007,166.635422,50640400 +2018-01-30,165.529999,167.369995,164.699997,166.970001,165.653244,46048200 +2018-01-31,166.869995,168.440002,166.5,167.429993,166.109604,32478900 +2018-02-01,167.169998,168.619995,166.759995,167.779999,166.456848,47230800 +2018-02-02,166,166.800003,160.100006,160.5,159.234253,86593800 +2018-02-05,159.100006,163.880005,156,156.490005,155.25589,72738500 +2018-02-06,154.830002,163.720001,154,163.029999,161.744293,68243800 +2018-02-07,163.089996,163.399994,159.070007,159.539993,158.281815,51608600 +2018-02-08,160.289993,161,155.029999,155.149994,153.926437,54390500 +2018-02-09,157.070007,157.889999,150.240005,156.410004,155.809189,70672600 +2018-02-12,158.5,163.889999,157.509995,162.710007,162.084991,60819500 +2018-02-13,161.949997,164.75,161.649994,164.339996,163.708725,32549200 +2018-02-14,163.039993,167.539993,162.880005,167.369995,166.727081,40644900 +2018-02-15,169.789993,173.089996,169,172.990005,172.3255,51147200 +2018-02-16,172.360001,174.820007,171.770004,172.429993,171.767639,40176100 +2018-02-20,172.050003,174.259995,171.419998,171.850006,171.18988,33930500 +2018-02-21,172.830002,174.119995,171.009995,171.070007,170.412872,37471600 +2018-02-22,171.800003,173.949997,171.710007,172.5,171.837372,30991900 +2018-02-23,173.669998,175.649994,173.539993,175.5,174.825851,33812400 +2018-02-26,176.350006,179.389999,176.210007,178.970001,178.282532,38162200 +2018-02-27,179.100006,180.479996,178.160004,178.389999,177.704758,38928100 +2018-02-28,179.259995,180.619995,178.050003,178.119995,177.435791,37782100 +2018-03-01,178.539993,179.779999,172.660004,175,174.327774,48802000 +2018-03-02,172.800003,176.300003,172.449997,176.210007,175.533142,38454000 +2018-03-05,175.210007,177.740005,174.520004,176.820007,176.140793,28401400 +2018-03-06,177.910004,178.25,176.130005,176.669998,175.991364,23788500 +2018-03-07,174.940002,175.850006,174.270004,175.029999,174.357666,31703500 +2018-03-08,175.479996,177.119995,175.070007,176.940002,176.26033,23774100 +2018-03-09,177.960007,180,177.389999,179.979996,179.288635,32185200 +2018-03-12,180.289993,182.389999,180.210007,181.720001,181.021957,32207100 +2018-03-13,182.589996,183.5,179.240005,179.970001,179.278687,31693500 +2018-03-14,180.320007,180.520004,177.809998,178.440002,177.754562,29368400 +2018-03-15,178.5,180.240005,178.070007,178.649994,177.963745,22743800 +2018-03-16,178.649994,179.119995,177.619995,178.020004,177.336182,39404700 +2018-03-19,177.320007,177.470001,173.660004,175.300003,174.626633,33446800 +2018-03-20,175.240005,176.800003,174.940002,175.240005,174.566864,19649400 +2018-03-21,175.039993,175.089996,171.259995,171.270004,170.612106,37054900 +2018-03-22,170,172.679993,168.600006,168.850006,168.201401,41490800 +2018-03-23,168.389999,169.919998,164.940002,164.940002,164.306427,41028800 +2018-03-26,168.070007,173.100006,166.440002,172.770004,172.106354,37541200 +2018-03-27,173.679993,175.149994,166.919998,168.339996,167.693359,40922600 +2018-03-28,167.25,170.020004,165.190002,166.479996,165.8405,41668500 +2018-03-29,167.809998,171.75,166.899994,167.779999,167.135513,38398500 +2018-04-02,166.639999,168.940002,164.470001,166.679993,166.039734,37586800 +2018-04-03,167.639999,168.75,164.880005,168.389999,167.743164,30278000 +2018-04-04,164.880005,172.009995,164.770004,171.610001,170.950806,34605500 +2018-04-05,172.580002,174.229996,172.080002,172.800003,172.13623,26933200 +2018-04-06,170.970001,172.479996,168.199997,168.380005,167.733215,35005300 +2018-04-09,169.880005,173.089996,169.850006,170.050003,169.39679,29017700 +2018-04-10,173,174,171.529999,173.25,172.584503,28408600 +2018-04-11,172.229996,173.919998,171.699997,172.440002,171.777618,22431600 +2018-04-12,173.410004,175,173.039993,174.139999,173.471085,22889300 +2018-04-13,174.779999,175.839996,173.850006,174.729996,174.058807,25124300 +2018-04-16,175.029999,176.190002,174.830002,175.820007,175.144638,21578400 +2018-04-17,176.490005,178.940002,176.410004,178.240005,177.555328,26605400 +2018-04-18,177.809998,178.820007,176.880005,177.839996,177.15686,20754500 +2018-04-19,173.759995,175.389999,172.660004,172.800003,172.13623,34808800 +2018-04-20,170.600006,171.220001,165.429993,165.720001,165.08342,65491100 +2018-04-23,166.830002,166.919998,164.089996,165.240005,164.60527,36515500 +2018-04-24,165.669998,166.330002,161.220001,162.940002,162.314102,33692000 +2018-04-25,162.619995,165.419998,162.410004,163.649994,163.021362,28382100 +2018-04-26,164.119995,165.729996,163.369995,164.220001,163.589188,27963000 +2018-04-27,164,164.330002,160.630005,162.320007,161.696487,35655800 +2018-04-30,162.130005,167.259995,161.839996,165.259995,164.625183,42427400 +2018-05-01,166.410004,169.199997,165.270004,169.100006,168.450439,53569400 +2018-05-02,175.229996,177.75,173.800003,176.570007,175.891754,66539400 +2018-05-03,175.880005,177.5,174.440002,176.889999,176.21051,34068200 +2018-05-04,178.25,184.25,178.169998,183.830002,183.123856,56201300 +2018-05-07,185.179993,187.669998,184.75,185.160004,184.448746,42451400 +2018-05-08,184.990005,186.220001,183.669998,186.050003,185.335327,28402800 +2018-05-09,186.550003,187.399994,185.220001,187.360001,186.640305,23211200 +2018-05-10,187.740005,190.369995,187.649994,190.039993,189.309998,27989300 +2018-05-11,189.490005,190.059998,187.449997,188.589996,188.589996,25806600 \ No newline at end of file diff --git a/src/content/tutorial/01-basics/02-scales/01-plot/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/02-scales/01-plot/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..13e9ba666 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/01-plot/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/content/tutorial/01-basics/02-scales/01-plot/index.md b/src/content/tutorial/01-basics/02-scales/01-plot/index.md new file mode 100644 index 000000000..2f7f565b3 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/01-plot/index.md @@ -0,0 +1,5 @@ +--- +title: The Plot component +--- + +In this chapter we're going to learn more about the `Plot` component. It's purpose is to take care of all the complicated aspects of creating visualizations: setting up common scales, axes, legends, and so on. You can customize all of these things, but the defaults are designed to work well for most charts. diff --git a/src/content/tutorial/01-basics/02-scales/02-extending-axes/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/02-scales/02-extending-axes/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..13e9ba666 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/02-extending-axes/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/content/tutorial/01-basics/02-scales/02-extending-axes/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/02-scales/02-extending-axes/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..89e22b115 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/02-extending-axes/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/content/tutorial/01-basics/02-scales/02-extending-axes/index.md b/src/content/tutorial/01-basics/02-scales/02-extending-axes/index.md new file mode 100644 index 000000000..be5f46929 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/02-extending-axes/index.md @@ -0,0 +1,24 @@ +--- +title: Extending the domain +--- + +Here's a line chart of Apple's stock closing price from 2013 to 2018. Notice how the y axis starts around $60 — SveltePlot fits the scale tightly to the data by default. + +That's usually sensible, but for some charts you want to anchor the y axis at zero to show the true magnitude of values. You can do this simply by adding a reference line at zero: + +First we need to add `RuleY` to the list of imports: + +```js +import { Plot, Line+++, RuleY+++ } from 'svelteplot'; +``` + +Now we can add `` to the plot: + +```svelte + + + ++++++ + +``` + +`` draws a horizontal rule at y = 0. The Plot component "collects" the data from all marks and extends the y axis to the data range. diff --git a/src/content/tutorial/01-basics/02-scales/03-scale-options/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/02-scales/03-scale-options/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..13e9ba666 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/03-scale-options/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/content/tutorial/01-basics/02-scales/03-scale-options/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/02-scales/03-scale-options/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..13dbfc526 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/03-scale-options/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/content/tutorial/01-basics/02-scales/03-scale-options/index.md b/src/content/tutorial/01-basics/02-scales/03-scale-options/index.md new file mode 100644 index 000000000..2aca3e390 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/03-scale-options/index.md @@ -0,0 +1,13 @@ +--- +title: Scale options +--- + +In the previous lesson you extended the y scale by adding a `` mark. That adds a visible line. If you want to anchor the axis at zero _without_ drawing a reference line, use the `zero` scale option instead: + +```svelte + +``` + +`zero: true` tells SveltePlot to always include zero in the y domain, no matter what the data looks like. The same option works on the x scale too. + +Scale options are passed as an object to the corresponding scale prop (`x`, `y`, `color`, `r`, etc.) on ``. You've already seen one: `grid: true` adds grid lines to that scale's axis. diff --git a/src/content/tutorial/01-basics/02-scales/04-scale-type/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/02-scales/04-scale-type/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..13e9ba666 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/04-scale-type/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/src/content/tutorial/01-basics/02-scales/04-scale-type/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/02-scales/04-scale-type/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..c0efff616 --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/04-scale-type/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/content/tutorial/01-basics/02-scales/04-scale-type/index.md b/src/content/tutorial/01-basics/02-scales/04-scale-type/index.md new file mode 100644 index 000000000..5797b0dab --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/04-scale-type/index.md @@ -0,0 +1,30 @@ +--- +title: Scale types +--- + +SveltePlot infers the **type** of each scale from the values it receives across all marks. The `Date` column in our AAPL data contains date strings, so SveltePlot picks a **temporal** (time-based) x scale automatically. + +Let's add a vertical reference line at the start of 2016. Import `RuleX` and add it to the plot: + +```svelte +import { Plot, Line, +++RuleX+++ } from 'svelteplot'; +``` + +Let's add a rule at `2016`: + +```svelte + ++ + + + + +``` diff --git a/src/content/tutorial/01-basics/02-scales/index.md b/src/content/tutorial/01-basics/02-scales/index.md new file mode 100644 index 000000000..1ca62affd --- /dev/null +++ b/src/content/tutorial/01-basics/02-scales/index.md @@ -0,0 +1,5 @@ +--- +title: Scales +scope: { 'prefix': '/src/lib/', 'name': 'src' } +focus: /src/lib/App.svelte +--- diff --git a/src/content/tutorial/01-basics/03-transforms/01-stacking/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/03-transforms/01-stacking/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..80d3963ac --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/01-stacking/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/content/tutorial/01-basics/03-transforms/01-stacking/+assets/app-a/src/lib/fruit-sales.csv b/src/content/tutorial/01-basics/03-transforms/01-stacking/+assets/app-a/src/lib/fruit-sales.csv new file mode 100644 index 000000000..11a16c9d0 --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/01-stacking/+assets/app-a/src/lib/fruit-sales.csv @@ -0,0 +1,9 @@ +Quarter,Fruit,Sales +Q1,Apples,1448 +Q2,Apples,1212 +Q3,Apples,1565 +Q4,Apples,1645 +Q1,Bananas,767 +Q2,Bananas,853 +Q3,Bananas,819 +Q4,Bananas,700 diff --git a/src/content/tutorial/01-basics/03-transforms/01-stacking/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/03-transforms/01-stacking/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..bab8de0aa --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/01-stacking/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,9 @@ + + + + + + diff --git a/src/content/tutorial/01-basics/03-transforms/01-stacking/index.md b/src/content/tutorial/01-basics/03-transforms/01-stacking/index.md new file mode 100644 index 000000000..ee370d5b5 --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/01-stacking/index.md @@ -0,0 +1,17 @@ +--- +title: Transforms +--- + +Transforms are useful when your dataset is not in the shape you need it to be visualized. In the chart you see fruit sales numbers represented as bars ranging from zero to more than 2000 sales units. + +But if you take a look at the dataset, you'll find the largest number to be the Q1 Apple sales at 1448 units. How is that? + +SveltePlot's BarY mark automatically (or implicitely) applied a **stackY transform**! It groups all dataset rows by the `x` values and stacks their `y` values on top of each other. + +This happens whenever you only pass a `y` channel instead of a `y1`/`y2` range, which is what the Bar mark actually visualizes. + +Try changing the `y="Sales"` to `y1={0} y2="Sales"` to see what happens without the stacking: + +```svelte + +``` diff --git a/src/content/tutorial/01-basics/03-transforms/06-jitter/+assets/src/lib/cars.csv b/src/content/tutorial/01-basics/03-transforms/06-jitter/+assets/src/lib/cars.csv new file mode 100644 index 000000000..96e2b22b2 --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/06-jitter/+assets/src/lib/cars.csv @@ -0,0 +1,407 @@ +manufactor,model,economy (mpg),cylinders,displacement (cc),power (hp),weight (lb),0-60 mph (s),year +AMC,Ambassador Brougham,13,8,360,175,3821,11,73 +AMC,Ambassador DPL,15,8,390,190,3850,8.5,70 +AMC,Ambassador SST,17,8,304,150,3672,11.5,72 +AMC,Concord DL 6,20.2,6,232,90,3265,18.2,79 +AMC,Concord DL,18.1,6,258,120,3410,15.1,78 +AMC,Concord DL,23,4,151,NaN,3035,20.5,82 +AMC,Concord,19.4,6,232,90,3210,17.2,78 +AMC,Concord,24.3,4,151,90,3003,20.1,80 +AMC,Gremlin,18,6,232,100,2789,15,73 +AMC,Gremlin,19,6,232,100,2634,13,71 +AMC,Gremlin,20,6,232,100,2914,16,75 +AMC,Gremlin,21,6,199,90,2648,15,70 +AMC,Hornet Sportabout (Wagon),18,6,258,110,2962,13.5,71 +AMC,Hornet,18,6,199,97,2774,15.5,70 +AMC,Hornet,18,6,232,100,2945,16,73 +AMC,Hornet,19,6,232,100,2901,16,74 +AMC,Hornet,22.5,6,232,90,3085,17.6,76 +AMC,Matador (Wagon),14,8,304,150,4257,15.5,74 +AMC,Matador (Wagon),15,8,304,150,3892,12.5,72 +AMC,Matador,14,8,304,150,3672,11.5,73 +AMC,Matador,15,6,258,110,3730,19,75 +AMC,Matador,15.5,8,304,120,3962,13.9,76 +AMC,Matador,16,6,258,110,3632,18,74 +AMC,Matador,18,6,232,100,3288,15.5,71 +AMC,Pacer D/L,17.5,6,258,95,3193,17.8,76 +AMC,Pacer,19,6,232,90,3211,17,75 +AMC,Rebel SST (Wagon),NaN,8,360,175,3850,11,70 +AMC,Rebel SST,16,8,304,150,3433,12,70 +AMC,Spirit DL,27.4,4,121,80,2670,15,79 +Audi,100 LS,20,4,114,91,2582,14,73 +Audi,100 LS,23,4,115,95,2694,15,75 +Audi,100 LS,24,4,107,90,2430,14.5,70 +Audi,4000,34.3,4,97,78,2188,15.8,80 +Audi,5000,20.3,5,131,103,2830,15.9,78 +Audi,5000S (Diesel),36.4,5,121,67,2950,19.9,80 +Audi,Fox,29,4,98,83,2219,16.5,74 +BMW,2002,26,4,121,113,2234,12.5,70 +BMW,320i,21.5,4,121,110,2600,12.8,77 +Buick,Century 350,13,8,350,175,4100,13,73 +Buick,Century Limited,25,6,181,110,2945,16.4,82 +Buick,Century Luxus (Wagon),13,8,350,150,4699,14.5,74 +Buick,Century Special,20.6,6,231,105,3380,15.8,78 +Buick,Century,17,6,231,110,3907,21,75 +Buick,Century,22.4,6,231,110,3415,15.8,81 +Buick,Electra 225 Custom,12,8,455,225,4951,11,73 +Buick,Estate Wagon (Wagon),14,8,455,225,3086,10,70 +Buick,Estate Wagon (Wagon),16.9,8,350,155,4360,14.9,79 +Buick,Lesabre Custom,13,8,350,155,4502,13.5,72 +Buick,Opel Isuzu Deluxe,30,4,111,80,2155,14.8,77 +Buick,Regal Sport Coupe (Turbo),17.7,6,231,165,3445,13.4,78 +Buick,Skyhawk,21,6,231,110,3039,15,75 +Buick,Skylark 320,15,8,350,165,3693,11.5,70 +Buick,Skylark Limited,28.4,4,151,90,2670,16,79 +Buick,Skylark,20.5,6,231,105,3425,16.9,77 +Buick,Skylark,26.6,4,151,84,2635,16.4,81 +Cadillac,Eldorado,23,8,350,125,3900,17.4,79 +Cadillac,Seville,16.5,8,350,180,4380,12.1,76 +Chevrolet,Chevelle Malibu,16,6,250,105,3897,18.5,75 +Chevrolet,Bel Air,15,8,350,145,4440,14,75 +Chevrolet,Camaro,27,4,151,90,2950,17.3,82 +Chevrolet,Caprice Classic,13,8,400,150,4464,12,73 +Chevrolet,Caprice Classic,17,8,305,130,3840,15.4,79 +Chevrolet,Caprice Classic,17.5,8,305,145,3880,12.5,77 +Chevrolet,Cavalier 2-Door,34,4,112,88,2395,18,82 +Chevrolet,Cavalier Wagon,27,4,112,88,2640,18.6,82 +Chevrolet,Cavalier,28,4,112,88,2605,19.6,82 +Chevrolet,Chevelle Concours (Wagon),NaN,8,350,165,4142,11.5,70 +Chevrolet,Chevelle Concours (Wagon),13,8,307,130,4098,14,72 +Chevrolet,Chevelle Malibu Classic,16,6,250,100,3781,17,74 +Chevrolet,Chevelle Malibu Classic,17.5,8,305,140,4215,13,76 +Chevrolet,Chevelle Malibu,17,6,250,100,3329,15.5,71 +Chevrolet,Chevelle Malibu,18,8,307,130,3504,12,70 +Chevrolet,Chevette,29,4,85,52,2035,22.2,76 +Chevrolet,Chevette,30,4,98,68,2155,16.5,78 +Chevrolet,Chevette,30.5,4,98,63,2051,17,77 +Chevrolet,Chevette,32.1,4,98,70,2120,15.5,80 +Chevrolet,Citation,23.5,6,173,110,2725,12.6,81 +Chevrolet,Citation,28,4,151,90,2678,16.5,80 +Chevrolet,Citation,28.8,6,173,115,2595,11.3,79 +Chevrolet,Concours,17.5,6,250,110,3520,16.4,77 +Chevrolet,Impala,11,8,400,150,4997,14,73 +Chevrolet,Impala,13,8,350,165,4274,12,72 +Chevrolet,Impala,14,8,350,165,4209,12,71 +Chevrolet,Impala,14,8,454,220,4354,9,70 +Chevrolet,Malibu Classic (Wagon),19.2,8,267,125,3605,15,79 +Chevrolet,Malibu,13,8,350,145,3988,13,73 +Chevrolet,Malibu,20.5,6,200,95,3155,18.2,78 +Chevrolet,Monte Carlo Landau,15.5,8,350,170,4165,11.4,77 +Chevrolet,Monte Carlo Landau,19.2,8,305,145,3425,13.2,78 +Chevrolet,Monte Carlo S,15,8,350,145,4082,13,73 +Chevrolet,Monte Carlo,15,8,400,150,3761,9.5,70 +Chevrolet,Monza 2+2,20,8,262,110,3221,13.5,75 +Chevrolet,Nova Custom,16,6,250,100,3278,18,73 +Chevrolet,Nova,15,6,250,100,3336,17,74 +Chevrolet,Nova,18,6,250,105,3459,16,75 +Chevrolet,Nova,22,6,250,105,3353,14.5,76 +Chevrolet,Vega (Wagon),22,4,140,72,2408,19,71 +Chevrolet,Vega 2300,28,4,140,90,2264,15.5,71 +Chevrolet,Vega,20,4,140,90,2408,19.5,72 +Chevrolet,Vega,21,4,140,72,2401,19.5,73 +Chevrolet,Vega,25,4,140,75,2542,17,74 +Chevrolet,Woody,24.5,4,98,60,2164,22.1,76 +Chevy,C10,13,8,350,145,4055,12,76 +Chevy,C20,10,8,307,200,4376,15,70 +Chevy,S-10,31,4,119,82,2720,19.4,82 +Chrysler,Cordoba,15.5,8,400,190,4325,12.2,77 +Chrysler,Lebaron Medallion,26,4,156,92,2585,14.5,82 +Chrysler,Lebaron Salon,17.6,6,225,85,3465,16.6,81 +Chrysler,Lebaron Town & Country (Wagon),18.5,8,360,150,3940,13,79 +Chrysler,New Yorker Brougham,13,8,440,215,4735,11,73 +Chrysler,Newport Royal,13,8,400,190,4422,12.5,72 +Citroen,DS-21 Pallas,NaN,4,133,115,3090,17.5,70 +Datsun,1200,35,4,72,69,1613,18,71 +Datsun,200SX,23.9,4,119,97,2405,14.9,78 +Datsun,200SX,32.9,4,119,100,2615,14.8,81 +Datsun,210,31.8,4,85,65,2020,19.2,79 +Datsun,210,37,4,85,65,1975,19.4,81 +Datsun,210,40.8,4,85,65,2110,19.2,80 +Datsun,280ZX,32.7,6,168,132,2910,11.4,80 +Datsun,310 GX,38,4,91,67,1995,16.2,82 +Datsun,310,37.2,4,86,65,2019,16.4,80 +Datsun,510 (Wagon),28,4,97,92,2288,17,72 +Datsun,510 Hatchback,37,4,119,92,2434,15,80 +Datsun,510,27.2,4,119,97,2300,14.7,78 +Datsun,610,22,4,108,94,2379,16.5,73 +Datsun,710,24,4,119,97,2545,17,75 +Datsun,710,32,4,83,61,2003,19,74 +Datsun,810 Maxima,24.2,6,146,120,2930,13.8,81 +Datsun,810,22,6,146,97,2815,14.5,77 +Datsun,B-210,32,4,85,70,1990,17,76 +Datsun,B210 GX,39.4,4,85,70,2070,18.6,78 +Datsun,B210,31,4,79,67,1950,19,74 +Datsun,F-10 Hatchback,33.5,4,85,70,1945,16.8,77 +Datsun,PL510,27,4,97,88,2130,14.5,70 +Datsun,PL510,27,4,97,88,2130,14.5,71 +Dodge,Aries SE,29,4,135,84,2525,16,82 +Dodge,Aries Wagon (Wagon),25.8,4,156,92,2620,14.4,81 +Dodge,Aspen 6,20.6,6,225,110,3360,16.6,79 +Dodge,Aspen SE,20,6,225,100,3651,17.7,76 +Dodge,Aspen,18.6,6,225,110,3620,18.7,78 +Dodge,Aspen,19.1,6,225,90,3381,18.7,80 +Dodge,Challenger SE,15,8,383,170,3563,10,70 +Dodge,Charger 2.2,36,4,135,84,2370,13,82 +Dodge,Colt (Wagon),28,4,98,80,2164,15,72 +Dodge,Colt Hardtop,25,4,97.5,80,2126,17,72 +Dodge,Colt Hatchback Custom,35.7,4,98,80,1915,14.4,79 +Dodge,Colt M/M,33.5,4,98,83,2075,15.9,77 +Dodge,Colt,26,4,98,79,2255,17.7,76 +Dodge,Colt,27.9,4,156,105,2800,14.4,80 +Dodge,Colt,28,4,90,75,2125,14.5,74 +Dodge,Coronet Brougham,16,8,318,150,4190,13,76 +Dodge,Coronet Custom (Wagon),14,8,318,150,4457,13.5,74 +Dodge,Coronet Custom,15,8,318,150,3777,12.5,73 +Dodge,D100,13,8,318,150,3755,14,76 +Dodge,D200,11,8,318,210,4382,13.5,70 +Dodge,Dart Custom,15,8,318,150,3399,11,73 +Dodge,Diplomat,19.4,8,318,140,3735,13.2,78 +Dodge,Magnum XE,17.5,8,318,140,4080,13.7,78 +Dodge,Monaco (Wagon),12,8,383,180,4955,11.5,71 +Dodge,Monaco Brougham,15.5,8,318,145,4140,13.7,77 +Dodge,Omni,30.9,4,105,75,2230,14.5,78 +Dodge,Rampage,32,4,135,84,2295,11.6,82 +Dodge,St. Regis,18.2,8,318,135,3830,15.2,79 +Fiat,124 Sport Coupe,26,4,98,90,2265,15.5,73 +Fiat,124 TC,26,4,116,75,2246,14,74 +Fiat,124B,30,4,88,76,2065,14.5,71 +Fiat,128,24,4,90,75,2108,15.5,74 +Fiat,128,29,4,68,49,1867,19.5,73 +Fiat,131,28,4,107,86,2464,15.5,76 +Fiat,Strada Custom,37.3,4,91,69,2130,14.7,79 +Fiat,X1.9,31,4,79,67,2000,16,74 +Ford,Capri II,25,4,140,92,2572,14.9,76 +Ford,Country Squire (Wagon),13,8,400,170,4746,12,71 +Ford,Country Squire (Wagon),15.5,8,351,142,4054,14.3,79 +Ford,Country,12,8,400,167,4906,12.5,73 +Ford,Escort 2H,29.9,4,98,65,2380,20.7,81 +Ford,Escort 4W,34.4,4,98,65,2045,16.2,81 +Ford,F108,13,8,302,130,3870,15,76 +Ford,F250,10,8,360,215,4615,14,70 +Ford,Fairmont (Auto),20.2,6,200,85,2965,15.8,78 +Ford,Fairmont (Man),25.1,4,140,88,2720,15.4,78 +Ford,Fairmont 4,22.3,4,140,88,2890,17.3,79 +Ford,Fairmont Futura,24,4,140,92,2865,16.4,82 +Ford,Fairmont,26.4,4,140,88,2870,18.1,80 +Ford,Fiesta,36.1,4,98,66,1800,14.4,78 +Ford,Futura,18.1,8,302,139,3205,11.2,78 +Ford,Galaxie 500,14,8,351,153,4129,13,72 +Ford,Galaxie 500,14,8,351,153,4154,13.5,71 +Ford,Galaxie 500,15,8,429,198,4341,10,70 +Ford,Gran Torino (Wagon),13,8,302,140,4294,16,72 +Ford,Gran Torino (Wagon),14,8,302,140,4638,16,74 +Ford,Gran Torino,14,8,302,137,4042,14.5,73 +Ford,Gran Torino,14.5,8,351,152,4215,12.8,76 +Ford,Gran Torino,16,8,302,140,4141,14,74 +Ford,Granada Ghia,18,6,250,78,3574,21,76 +Ford,Granada GL,20.2,6,200,88,3060,17.1,81 +Ford,Granada L,22,6,232,112,2835,14.7,82 +Ford,Granada,18.5,6,250,98,3525,19,77 +Ford,LTD Landau,17.6,8,302,129,3725,13.4,79 +Ford,LTD,13,8,351,158,4363,13,73 +Ford,LTD,14,8,351,148,4657,13.5,75 +Ford,Maverick,15,6,250,72,3158,19.5,75 +Ford,Maverick,18,6,250,88,3021,16.5,73 +Ford,Maverick,21,6,200,NaN,2875,17,74 +Ford,Maverick,21,6,200,85,2587,16,70 +Ford,Maverick,24,6,200,81,3012,17.6,76 +Ford,Mustang Boss 302,NaN,8,302,140,3353,8,70 +Ford,Mustang Cobra,23.6,4,140,NaN,2905,14.3,80 +Ford,Mustang GL,27,4,140,86,2790,15.6,82 +Ford,Mustang II 2+2,25.5,4,140,89,2755,15.8,77 +Ford,Mustang II,13,8,302,129,3169,12,75 +Ford,Mustang,18,6,250,88,3139,14.5,71 +Ford,Pinto (Wagon),22,4,122,86,2395,16,72 +Ford,Pinto Runabout,21,4,122,86,2226,16.5,72 +Ford,Pinto,18,6,171,97,2984,14.5,75 +Ford,Pinto,19,4,122,85,2310,18.5,73 +Ford,Pinto,23,4,140,83,2639,17,75 +Ford,Pinto,25,4,98,NaN,2046,19,71 +Ford,Pinto,26,4,122,80,2451,16.5,74 +Ford,Pinto,26.5,4,140,72,2565,13.6,76 +Ford,Ranger,28,4,120,79,2625,18.6,82 +Ford,Thunderbird,16,8,351,149,4335,14.5,77 +Ford,Torino (Wagon),NaN,8,351,153,4034,11,70 +Ford,Torino 500,19,6,250,88,3302,15.5,71 +Ford,Torino,17,8,302,140,3449,10.5,70 +Hi,1200D,9,8,304,193,4732,18.5,70 +Honda,Accord CVCC,31.5,4,98,68,2045,18.5,77 +Honda,Accord LX,29.5,4,98,68,2135,16.6,78 +Honda,Accord,32.4,4,107,72,2290,17,80 +Honda,Accord,36,4,107,75,2205,14.5,82 +Honda,Civic (Auto),32,4,91,67,1965,15.7,82 +Honda,Civic 1300,35.1,4,81,60,1760,16.1,81 +Honda,Civic 1500 GL,44.6,4,91,67,1850,13.8,80 +Honda,Civic CVCC,33,4,91,53,1795,17.5,75 +Honda,Civic CVCC,36.1,4,91,60,1800,16.4,78 +Honda,Civic,24,4,120,97,2489,15,74 +Honda,Civic,33,4,91,53,1795,17.4,76 +Honda,Civic,38,4,91,67,1965,15,82 +Honda,Prelude,33.7,4,107,75,2210,14.4,81 +Mazda,GLC Deluxe,34.1,4,86,65,1975,15.2,79 +Mazda,RX-3,18,3,70,90,2124,13.5,73 +Mazda,626,31.3,4,120,75,2542,17.5,80 +Mazda,626,31.6,4,120,74,2635,18.3,81 +Mazda,GLC 4,34.1,4,91,68,1985,16,81 +Mazda,GLC Custom L,37,4,91,68,2025,18.2,82 +Mazda,GLC Custom,31,4,91,68,1970,17.6,82 +Mazda,GLC Deluxe,32.8,4,78,52,1985,19.4,78 +Mazda,GLC,46.6,4,86,65,2110,17.9,80 +Mazda,RX-2 Coupe,19,3,70,97,2330,13.5,72 +Mazda,RX-4,21.5,3,80,110,2720,13.5,77 +Mazda,RX-7 Gs,23.7,3,70,100,2420,12.5,80 +Mercedes-Benz,240D,30,4,146,67,3250,21.8,80 +Mercedes-Benz,280S,16.5,6,168,120,3820,16.7,76 +Mercedes-Benz,300D,25.4,5,183,77,3530,20.1,79 +Mercury,Capri 2000,23,4,122,86,2220,14,71 +Mercury,Capri V6,21,6,155,107,2472,14,73 +Mercury,Cougar Brougham,15,8,302,130,4295,14.9,77 +Mercury,Grand Marquis,16.5,8,351,138,3955,13.2,79 +Mercury,Lynx L,36,4,98,70,2125,17.3,82 +Mercury,Marquis Brougham,12,8,429,198,4952,11.5,73 +Mercury,Marquis,11,8,429,208,4633,11,72 +Mercury,Monarch Ghia,20.2,8,302,139,3570,12.8,78 +Mercury,Monarch,15,6,250,72,3432,21,75 +Mercury,Zephyr 6,19.8,6,200,85,2990,18.2,79 +Mercury,Zephyr,20.8,6,200,85,3070,16.7,78 +Nissan,Stanza XE,36,4,120,88,2160,14.5,82 +Oldsmobile,Cutlass Ciera (Diesel),38,6,262,85,3015,17,82 +Oldsmobile,Cutlass LS,26.6,8,350,105,3725,19,81 +Oldsmobile,Cutlass Salon Brougham,19.9,8,260,110,3365,15.5,78 +Oldsmobile,Cutlass Salon Brougham,23.9,8,260,90,3420,22.2,79 +Oldsmobile,Cutlass Supreme,17,8,260,110,4060,19,77 +Oldsmobile,Delta 88 Royale,12,8,350,160,4456,13.5,72 +Oldsmobile,Omega Brougham,26.8,6,173,115,2700,12.9,79 +Oldsmobile,Omega,11,8,350,180,3664,11,73 +Oldsmobile,Starfire SX,23.8,4,151,85,2855,17.6,78 +Oldsmobile,Vista Cruiser,12,8,350,180,4499,12.5,73 +Opel,1900,25,4,116,81,2220,16.9,76 +Opel,1900,28,4,116,90,2123,14,71 +Opel,Manta,24,4,116,75,2158,15.5,73 +Opel,Manta,26,4,97,78,2300,14.5,74 +Peugeot,304,30,4,79,70,2074,19.5,71 +Peugeot,504 (Wagon),21,4,120,87,2979,19.5,72 +Peugeot,504,19,4,120,88,3270,21.9,76 +Peugeot,504,23,4,120,88,2957,17,75 +Peugeot,504,25,4,110,87,2672,17.5,70 +Peugeot,504,27.2,4,141,71,3190,24.8,79 +Peugeot,505S Turbo Diesel,28.1,4,141,80,3230,20.4,81 +Peugeot,604SL,16.2,6,163,133,3410,15.8,78 +Plymouth,Arrow GS,25.5,4,122,96,2300,15.5,77 +Plymouth,Barracuda 340,14,8,340,160,3609,8,70 +Plymouth,Champ,39,4,86,64,1875,16.4,81 +Plymouth,Cricket,26,4,91,70,1955,20.5,71 +Plymouth,Custom Suburb,13,8,360,170,4654,13,73 +Plymouth,Duster,20,6,198,95,3102,16.5,74 +Plymouth,Duster,22,6,198,95,2833,15.5,70 +Plymouth,Duster,23,6,198,95,2904,16,73 +Plymouth,Fury Gran Sedan,14,8,318,150,4237,14.5,73 +Plymouth,Fury III,14,8,318,150,4096,13,71 +Plymouth,Fury III,14,8,440,215,4312,8.5,70 +Plymouth,Fury III,15,8,318,150,4135,13.5,72 +Plymouth,Fury,18,6,225,95,3785,19,75 +Plymouth,Grand Fury,16,8,318,150,4498,14.5,75 +Plymouth,Horizon 4,34.7,4,105,63,2215,14.9,81 +Plymouth,Horizon Miser,38,4,105,63,2125,14.7,82 +Plymouth,Horizon TC3,34.5,4,105,70,2150,14.9,79 +Plymouth,Horizon,34.2,4,105,70,2200,13.2,79 +Plymouth,Reliant,27.2,4,135,84,2490,15.7,81 +Plymouth,Reliant,30,4,135,84,2385,12.9,81 +Plymouth,Sapporo,23.2,4,156,105,2745,16.7,78 +Plymouth,Satellite (Wagon),NaN,8,383,175,4166,10.5,70 +Plymouth,Satellite Custom (Wagon),14,8,318,150,4077,14,72 +Plymouth,Satellite Custom,16,6,225,105,3439,15.5,71 +Plymouth,Satellite Sebring,18,6,225,105,3613,16.5,74 +Plymouth,Satellite,18,8,318,150,3436,11,70 +Plymouth,Valiant Custom,19,6,225,95,3264,16,75 +Plymouth,Valiant,18,6,225,105,3121,16.5,73 +Plymouth,Valiant,22,6,225,100,3233,15.4,76 +Plymouth,Volare Custom,19,6,225,100,3630,17.7,77 +Plymouth,Volare Premier V8,13,8,318,150,3940,13.2,76 +Plymouth,Volare,20.5,6,225,100,3430,17.2,78 +Pontiac,Astro,23,4,140,78,2592,18.5,75 +Pontiac,Catalina Brougham,14,8,400,175,4464,11.5,71 +Pontiac,Catalina,14,8,400,175,4385,12,72 +Pontiac,Catalina,14,8,455,225,4425,10,70 +Pontiac,Catalina,16,8,400,170,4668,11.5,75 +Pontiac,Firebird,19,6,250,100,3282,15,71 +Pontiac,Grand Prix Lj,16,8,400,180,4220,11.1,77 +Pontiac,Grand Prix,16,8,400,230,4278,9.5,73 +Pontiac,J2000 Se Hatchback,31,4,112,85,2575,16.2,82 +Pontiac,Lemans V6,21.5,6,231,115,3245,15.4,79 +Pontiac,Phoenix LJ,19.2,6,231,105,3535,19.2,78 +Pontiac,Phoenix,27,4,151,90,2735,18,82 +Pontiac,Phoenix,33.5,4,151,90,2556,13.2,79 +Pontiac,Safari (Wagon),13,8,400,175,5140,12,71 +Pontiac,Sunbird Coupe,24.5,4,151,88,2740,16,77 +Pontiac,Ventura Sj,18.5,6,250,110,3645,16.2,76 +Renault,12 (Wagon),26,4,96,69,2189,18,72 +Renault,12TL,27,4,101,83,2202,15.3,76 +Renault,18I,34.5,4,100,NaN,2320,15.8,81 +Renault,5 Gtl,36,4,79,58,1825,18.6,77 +Renault,Lecar Deluxe,40.9,4,85,NaN,1835,17.3,80 +Saab,900S,NaN,4,121,110,2800,15.4,81 +Saab,99E,25,4,104,95,2375,17.5,70 +Saab,99GLE,21.6,4,121,115,2795,15.7,78 +Saab,99LE,24,4,121,110,2660,14,73 +Saab,99LE,25,4,121,115,2671,13.5,75 +Subaru,DL,30,4,97,67,1985,16.4,77 +Subaru,DL,33.8,4,97,67,2145,18,80 +Subaru,,26,4,108,93,2391,15.5,74 +Subaru,,32.3,4,97,67,2065,17.8,81 +Toyota,Carina,20,4,97,88,2279,19,73 +Toyota,Celica GT Liftback,21.1,4,134,95,2515,14.8,78 +Toyota,Celica GT,32,4,144,96,2665,13.9,82 +Toyota,Corolla 1200,31,4,71,65,1773,19,71 +Toyota,Corolla 1200,32,4,71,65,1836,21,74 +Toyota,Corolla 1600 (Wagon),27,4,97,88,2100,16.5,72 +Toyota,Corolla Liftback,26,4,97,75,2265,18.2,77 +Toyota,Corolla Tercel,38.1,4,89,60,1968,18.8,80 +Toyota,Corolla,28,4,97,75,2155,16.4,76 +Toyota,Corolla,29,4,97,75,2171,16,75 +Toyota,Corolla,32.2,4,108,75,2265,15.2,80 +Toyota,Corolla,32.4,4,108,75,2350,16.8,81 +Toyota,Corolla,34,4,108,70,2245,16.9,82 +Toyota,Corona Hardtop,24,4,113,95,2278,15.5,72 +Toyota,Corona Liftback,29.8,4,134,90,2711,15.5,80 +Toyota,Corona Mark II,24,4,113,95,2372,15,70 +Toyota,Corona,24,4,134,96,2702,13.5,75 +Toyota,Corona,25,4,113,95,2228,14,71 +Toyota,Corona,27.5,4,134,95,2560,14.2,78 +Toyota,Corona,31,4,76,52,1649,16.5,74 +Toyota,Cressida,25.4,6,168,116,2900,12.6,81 +Toyota,Mark II,19,6,156,108,2930,15.5,76 +Toyota,Mark II,20,6,156,122,2807,13.5,73 +Toyota,Starlet,39.1,4,79,58,1755,16.9,81 +Toyota,Tercel,37.7,4,89,62,2050,17.3,81 +Toyota,Corona Mark II (Wagon),23,4,120,97,2506,14.5,72 +Triumph,TR7 Coupe,35,4,122,88,2500,15.1,80 +Volkswagen,Rabbit,29.8,4,89,62,1845,15.3,80 +Volkswagen,1131 Deluxe Sedan,26,4,97,46,1835,20.5,70 +Volkswagen,411 (Wagon),22,4,121,76,2511,18,72 +Volkswagen,Dasher (Diesel),43.4,4,90,48,2335,23.7,80 +Volkswagen,Dasher,25,4,90,71,2223,16.5,75 +Volkswagen,Dasher,26,4,79,67,1963,15.5,74 +Volkswagen,Dasher,30.5,4,97,78,2190,14.1,77 +Volkswagen,Jetta,33,4,105,74,2190,14.2,81 +Volkswagen,Model 111,27,4,97,60,1834,19,71 +Volkswagen,Pickup,44,4,97,52,2130,24.6,82 +Volkswagen,Rabbit C (Diesel),44.3,4,90,48,2085,21.7,80 +Volkswagen,Rabbit Custom Diesel,43.1,4,90,48,1985,21.5,78 +Volkswagen,Rabbit Custom,29,4,97,78,1940,14.5,77 +Volkswagen,Rabbit Custom,31.9,4,89,71,1925,14,79 +Volkswagen,Rabbit L,36,4,105,74,1980,15.3,82 +Volkswagen,Rabbit,29,4,90,70,1937,14,75 +Volkswagen,Rabbit,29,4,90,70,1937,14.2,76 +Volkswagen,Rabbit,29.5,4,97,71,1825,12.2,76 +Volkswagen,Rabbit,41.5,4,98,76,2144,14.7,80 +Volkswagen,Scirocco,31.5,4,89,71,1990,14.9,78 +Volkswagen,Super Beetle 117,NaN,4,97,48,1978,20,71 +Volkswagen,Super Beetle,26,4,97,46,1950,21,73 +Volkswagen,Type 3,23,4,97,54,2254,23.5,72 +Volvo,144EA,19,4,121,112,2868,15.5,73 +Volvo,145E (Wagon),18,4,121,112,2933,14.5,72 +Volvo,244DL,22,4,121,98,2945,14.5,75 +Volvo,245,20,4,130,102,3150,15.7,76 +Volvo,264GL,17,6,163,125,3140,13.6,78 +Volvo,Diesel,30.7,6,145,76,3160,19.6,81 \ No newline at end of file diff --git a/src/content/tutorial/01-basics/03-transforms/06-jitter/01-jitter/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/03-transforms/06-jitter/01-jitter/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..6ee7fa298 --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/06-jitter/01-jitter/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/src/content/tutorial/01-basics/03-transforms/06-jitter/01-jitter/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/03-transforms/06-jitter/01-jitter/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..6e91faf2d --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/06-jitter/01-jitter/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/content/tutorial/01-basics/03-transforms/06-jitter/01-jitter/index.md b/src/content/tutorial/01-basics/03-transforms/06-jitter/01-jitter/index.md new file mode 100644 index 000000000..47024b88f --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/06-jitter/01-jitter/index.md @@ -0,0 +1,27 @@ +--- +title: Jitter transform +--- + +When x values are discrete integers — like cylinder counts — points stack exactly on top of each other. Opacity helps a little, but you still can't tell how many points share a position. + +The `jitterX` transform adds a small random horizontal nudge to each point. Import it alongside your marks: + +```svelte +---import { Plot, Dot } from 'svelteplot';--- ++++import { Plot, Dot, jitterX } from 'svelteplot';+++ +``` + +Compute the jittered channels in a `$derived` block, then spread the result onto ``: + +```svelte ++++const jittered = $derived( + jitterX({ data, x: 'cylinders', y: 'power (hp)' }, { type: 'uniform', width: 0.45 }) +);+++ +``` + +```svelte +------ +++++++ +``` + +The `width` option controls how far points can spread (in data units — here ±0.45 cylinders). Only the positional channels go through the transform; any other props like `opacity` are passed directly to ``. diff --git a/src/content/tutorial/01-basics/03-transforms/06-jitter/02-reactive-jitter/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/03-transforms/06-jitter/02-reactive-jitter/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..6e91faf2d --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/06-jitter/02-reactive-jitter/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/content/tutorial/01-basics/03-transforms/06-jitter/02-reactive-jitter/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/03-transforms/06-jitter/02-reactive-jitter/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..1cb8a0372 --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/06-jitter/02-reactive-jitter/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,28 @@ + + + + + + + diff --git a/src/content/tutorial/01-basics/03-transforms/06-jitter/02-reactive-jitter/index.md b/src/content/tutorial/01-basics/03-transforms/06-jitter/02-reactive-jitter/index.md new file mode 100644 index 000000000..b8b4f019a --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/06-jitter/02-reactive-jitter/index.md @@ -0,0 +1,26 @@ +--- +title: Reactive jitter +--- + +Because `$derived` re-runs whenever its dependencies change, any reactive variable you reference inside the transform will update the chart automatically. + +Add a `$state` variable for the jitter width and wire it to a range input: + +```svelte ++++let width = $state(0.45);+++ +``` + +```svelte +const jittered = $derived( +--- jitterX({ data, x: 'cylinders', y: 'power (hp)' }, { type: 'uniform', width: 0.45 })--- ++++ jitterX({ data, x: 'cylinders', y: 'power (hp)' }, { type: 'uniform', width })+++ +); +``` + +```svelte +++++++ +``` + +Move the slider to control how spread out the jitter is. Since `$derived` calls `Math.random` anew on every re-run, the dot positions also reshuffle each time — the jitter is never the same twice. diff --git a/src/content/tutorial/01-basics/03-transforms/06-jitter/index.md b/src/content/tutorial/01-basics/03-transforms/06-jitter/index.md new file mode 100644 index 000000000..f44f2d143 --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/06-jitter/index.md @@ -0,0 +1,5 @@ +--- +title: Jitter +scope: { 'prefix': '/src/lib/', 'name': 'src' } +focus: /src/lib/App.svelte +--- diff --git a/src/content/tutorial/01-basics/03-transforms/index.md b/src/content/tutorial/01-basics/03-transforms/index.md new file mode 100644 index 000000000..cb1103a53 --- /dev/null +++ b/src/content/tutorial/01-basics/03-transforms/index.md @@ -0,0 +1,5 @@ +--- +title: Transforms +scope: { 'prefix': '/src/lib/', 'name': 'src' } +focus: /src/lib/App.svelte +--- diff --git a/src/content/tutorial/01-basics/05-axes-grids/+assets/src/lib/penguins.csv b/src/content/tutorial/01-basics/05-axes-grids/+assets/src/lib/penguins.csv new file mode 100644 index 000000000..83f32630b --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/+assets/src/lib/penguins.csv @@ -0,0 +1,345 @@ +species,island,bill_length_mm,bill_depth_mm,flipper_length_mm,body_mass_g,sex +Adelie,Torgersen,39.1,18.7,181,3750,MALE +Adelie,Torgersen,39.5,17.4,186,3800,FEMALE +Adelie,Torgersen,40.3,18,195,3250,FEMALE +Adelie,Torgersen,NaN,NaN,NaN,NaN, +Adelie,Torgersen,36.7,19.3,193,3450,FEMALE +Adelie,Torgersen,39.3,20.6,190,3650,MALE +Adelie,Torgersen,38.9,17.8,181,3625,FEMALE +Adelie,Torgersen,39.2,19.6,195,4675,MALE +Adelie,Torgersen,34.1,18.1,193,3475, +Adelie,Torgersen,42,20.2,190,4250, +Adelie,Torgersen,37.8,17.1,186,3300, +Adelie,Torgersen,37.8,17.3,180,3700, +Adelie,Torgersen,41.1,17.6,182,3200,FEMALE +Adelie,Torgersen,38.6,21.2,191,3800,MALE +Adelie,Torgersen,34.6,21.1,198,4400,MALE +Adelie,Torgersen,36.6,17.8,185,3700,FEMALE +Adelie,Torgersen,38.7,19,195,3450,FEMALE +Adelie,Torgersen,42.5,20.7,197,4500,MALE +Adelie,Torgersen,34.4,18.4,184,3325,FEMALE +Adelie,Torgersen,46,21.5,194,4200,MALE +Adelie,Biscoe,37.8,18.3,174,3400,FEMALE +Adelie,Biscoe,37.7,18.7,180,3600,MALE +Adelie,Biscoe,35.9,19.2,189,3800,FEMALE +Adelie,Biscoe,38.2,18.1,185,3950,MALE +Adelie,Biscoe,38.8,17.2,180,3800,MALE +Adelie,Biscoe,35.3,18.9,187,3800,FEMALE +Adelie,Biscoe,40.6,18.6,183,3550,MALE +Adelie,Biscoe,40.5,17.9,187,3200,FEMALE +Adelie,Biscoe,37.9,18.6,172,3150,FEMALE +Adelie,Biscoe,40.5,18.9,180,3950,MALE +Adelie,Dream,39.5,16.7,178,3250,FEMALE +Adelie,Dream,37.2,18.1,178,3900,MALE +Adelie,Dream,39.5,17.8,188,3300,FEMALE +Adelie,Dream,40.9,18.9,184,3900,MALE +Adelie,Dream,36.4,17,195,3325,FEMALE +Adelie,Dream,39.2,21.1,196,4150,MALE +Adelie,Dream,38.8,20,190,3950,MALE +Adelie,Dream,42.2,18.5,180,3550,FEMALE +Adelie,Dream,37.6,19.3,181,3300,FEMALE +Adelie,Dream,39.8,19.1,184,4650,MALE +Adelie,Dream,36.5,18,182,3150,FEMALE +Adelie,Dream,40.8,18.4,195,3900,MALE +Adelie,Dream,36,18.5,186,3100,FEMALE +Adelie,Dream,44.1,19.7,196,4400,MALE +Adelie,Dream,37,16.9,185,3000,FEMALE +Adelie,Dream,39.6,18.8,190,4600,MALE +Adelie,Dream,41.1,19,182,3425,MALE +Adelie,Dream,37.5,18.9,179,2975, +Adelie,Dream,36,17.9,190,3450,FEMALE +Adelie,Dream,42.3,21.2,191,4150,MALE +Adelie,Biscoe,39.6,17.7,186,3500,FEMALE +Adelie,Biscoe,40.1,18.9,188,4300,MALE +Adelie,Biscoe,35,17.9,190,3450,FEMALE +Adelie,Biscoe,42,19.5,200,4050,MALE +Adelie,Biscoe,34.5,18.1,187,2900,FEMALE +Adelie,Biscoe,41.4,18.6,191,3700,MALE +Adelie,Biscoe,39,17.5,186,3550,FEMALE +Adelie,Biscoe,40.6,18.8,193,3800,MALE +Adelie,Biscoe,36.5,16.6,181,2850,FEMALE +Adelie,Biscoe,37.6,19.1,194,3750,MALE +Adelie,Biscoe,35.7,16.9,185,3150,FEMALE +Adelie,Biscoe,41.3,21.1,195,4400,MALE +Adelie,Biscoe,37.6,17,185,3600,FEMALE +Adelie,Biscoe,41.1,18.2,192,4050,MALE +Adelie,Biscoe,36.4,17.1,184,2850,FEMALE +Adelie,Biscoe,41.6,18,192,3950,MALE +Adelie,Biscoe,35.5,16.2,195,3350,FEMALE +Adelie,Biscoe,41.1,19.1,188,4100,MALE +Adelie,Torgersen,35.9,16.6,190,3050,FEMALE +Adelie,Torgersen,41.8,19.4,198,4450,MALE +Adelie,Torgersen,33.5,19,190,3600,FEMALE +Adelie,Torgersen,39.7,18.4,190,3900,MALE +Adelie,Torgersen,39.6,17.2,196,3550,FEMALE +Adelie,Torgersen,45.8,18.9,197,4150,MALE +Adelie,Torgersen,35.5,17.5,190,3700,FEMALE +Adelie,Torgersen,42.8,18.5,195,4250,MALE +Adelie,Torgersen,40.9,16.8,191,3700,FEMALE +Adelie,Torgersen,37.2,19.4,184,3900,MALE +Adelie,Torgersen,36.2,16.1,187,3550,FEMALE +Adelie,Torgersen,42.1,19.1,195,4000,MALE +Adelie,Torgersen,34.6,17.2,189,3200,FEMALE +Adelie,Torgersen,42.9,17.6,196,4700,MALE +Adelie,Torgersen,36.7,18.8,187,3800,FEMALE +Adelie,Torgersen,35.1,19.4,193,4200,MALE +Adelie,Dream,37.3,17.8,191,3350,FEMALE +Adelie,Dream,41.3,20.3,194,3550,MALE +Adelie,Dream,36.3,19.5,190,3800,MALE +Adelie,Dream,36.9,18.6,189,3500,FEMALE +Adelie,Dream,38.3,19.2,189,3950,MALE +Adelie,Dream,38.9,18.8,190,3600,FEMALE +Adelie,Dream,35.7,18,202,3550,FEMALE +Adelie,Dream,41.1,18.1,205,4300,MALE +Adelie,Dream,34,17.1,185,3400,FEMALE +Adelie,Dream,39.6,18.1,186,4450,MALE +Adelie,Dream,36.2,17.3,187,3300,FEMALE +Adelie,Dream,40.8,18.9,208,4300,MALE +Adelie,Dream,38.1,18.6,190,3700,FEMALE +Adelie,Dream,40.3,18.5,196,4350,MALE +Adelie,Dream,33.1,16.1,178,2900,FEMALE +Adelie,Dream,43.2,18.5,192,4100,MALE +Adelie,Biscoe,35,17.9,192,3725,FEMALE +Adelie,Biscoe,41,20,203,4725,MALE +Adelie,Biscoe,37.7,16,183,3075,FEMALE +Adelie,Biscoe,37.8,20,190,4250,MALE +Adelie,Biscoe,37.9,18.6,193,2925,FEMALE +Adelie,Biscoe,39.7,18.9,184,3550,MALE +Adelie,Biscoe,38.6,17.2,199,3750,FEMALE +Adelie,Biscoe,38.2,20,190,3900,MALE +Adelie,Biscoe,38.1,17,181,3175,FEMALE +Adelie,Biscoe,43.2,19,197,4775,MALE +Adelie,Biscoe,38.1,16.5,198,3825,FEMALE +Adelie,Biscoe,45.6,20.3,191,4600,MALE +Adelie,Biscoe,39.7,17.7,193,3200,FEMALE +Adelie,Biscoe,42.2,19.5,197,4275,MALE +Adelie,Biscoe,39.6,20.7,191,3900,FEMALE +Adelie,Biscoe,42.7,18.3,196,4075,MALE +Adelie,Torgersen,38.6,17,188,2900,FEMALE +Adelie,Torgersen,37.3,20.5,199,3775,MALE +Adelie,Torgersen,35.7,17,189,3350,FEMALE +Adelie,Torgersen,41.1,18.6,189,3325,MALE +Adelie,Torgersen,36.2,17.2,187,3150,FEMALE +Adelie,Torgersen,37.7,19.8,198,3500,MALE +Adelie,Torgersen,40.2,17,176,3450,FEMALE +Adelie,Torgersen,41.4,18.5,202,3875,MALE +Adelie,Torgersen,35.2,15.9,186,3050,FEMALE +Adelie,Torgersen,40.6,19,199,4000,MALE +Adelie,Torgersen,38.8,17.6,191,3275,FEMALE +Adelie,Torgersen,41.5,18.3,195,4300,MALE +Adelie,Torgersen,39,17.1,191,3050,FEMALE +Adelie,Torgersen,44.1,18,210,4000,MALE +Adelie,Torgersen,38.5,17.9,190,3325,FEMALE +Adelie,Torgersen,43.1,19.2,197,3500,MALE +Adelie,Dream,36.8,18.5,193,3500,FEMALE +Adelie,Dream,37.5,18.5,199,4475,MALE +Adelie,Dream,38.1,17.6,187,3425,FEMALE +Adelie,Dream,41.1,17.5,190,3900,MALE +Adelie,Dream,35.6,17.5,191,3175,FEMALE +Adelie,Dream,40.2,20.1,200,3975,MALE +Adelie,Dream,37,16.5,185,3400,FEMALE +Adelie,Dream,39.7,17.9,193,4250,MALE +Adelie,Dream,40.2,17.1,193,3400,FEMALE +Adelie,Dream,40.6,17.2,187,3475,MALE +Adelie,Dream,32.1,15.5,188,3050,FEMALE +Adelie,Dream,40.7,17,190,3725,MALE +Adelie,Dream,37.3,16.8,192,3000,FEMALE +Adelie,Dream,39,18.7,185,3650,MALE +Adelie,Dream,39.2,18.6,190,4250,MALE +Adelie,Dream,36.6,18.4,184,3475,FEMALE +Adelie,Dream,36,17.8,195,3450,FEMALE +Adelie,Dream,37.8,18.1,193,3750,MALE +Adelie,Dream,36,17.1,187,3700,FEMALE +Adelie,Dream,41.5,18.5,201,4000,MALE +Chinstrap,Dream,46.5,17.9,192,3500,FEMALE +Chinstrap,Dream,50,19.5,196,3900,MALE +Chinstrap,Dream,51.3,19.2,193,3650,MALE +Chinstrap,Dream,45.4,18.7,188,3525,FEMALE +Chinstrap,Dream,52.7,19.8,197,3725,MALE +Chinstrap,Dream,45.2,17.8,198,3950,FEMALE +Chinstrap,Dream,46.1,18.2,178,3250,FEMALE +Chinstrap,Dream,51.3,18.2,197,3750,MALE +Chinstrap,Dream,46,18.9,195,4150,FEMALE +Chinstrap,Dream,51.3,19.9,198,3700,MALE +Chinstrap,Dream,46.6,17.8,193,3800,FEMALE +Chinstrap,Dream,51.7,20.3,194,3775,MALE +Chinstrap,Dream,47,17.3,185,3700,FEMALE +Chinstrap,Dream,52,18.1,201,4050,MALE +Chinstrap,Dream,45.9,17.1,190,3575,FEMALE +Chinstrap,Dream,50.5,19.6,201,4050,MALE +Chinstrap,Dream,50.3,20,197,3300,MALE +Chinstrap,Dream,58,17.8,181,3700,FEMALE +Chinstrap,Dream,46.4,18.6,190,3450,FEMALE +Chinstrap,Dream,49.2,18.2,195,4400,MALE +Chinstrap,Dream,42.4,17.3,181,3600,FEMALE +Chinstrap,Dream,48.5,17.5,191,3400,MALE +Chinstrap,Dream,43.2,16.6,187,2900,FEMALE +Chinstrap,Dream,50.6,19.4,193,3800,MALE +Chinstrap,Dream,46.7,17.9,195,3300,FEMALE +Chinstrap,Dream,52,19,197,4150,MALE +Chinstrap,Dream,50.5,18.4,200,3400,FEMALE +Chinstrap,Dream,49.5,19,200,3800,MALE +Chinstrap,Dream,46.4,17.8,191,3700,FEMALE +Chinstrap,Dream,52.8,20,205,4550,MALE +Chinstrap,Dream,40.9,16.6,187,3200,FEMALE +Chinstrap,Dream,54.2,20.8,201,4300,MALE +Chinstrap,Dream,42.5,16.7,187,3350,FEMALE +Chinstrap,Dream,51,18.8,203,4100,MALE +Chinstrap,Dream,49.7,18.6,195,3600,MALE +Chinstrap,Dream,47.5,16.8,199,3900,FEMALE +Chinstrap,Dream,47.6,18.3,195,3850,FEMALE +Chinstrap,Dream,52,20.7,210,4800,MALE +Chinstrap,Dream,46.9,16.6,192,2700,FEMALE +Chinstrap,Dream,53.5,19.9,205,4500,MALE +Chinstrap,Dream,49,19.5,210,3950,MALE +Chinstrap,Dream,46.2,17.5,187,3650,FEMALE +Chinstrap,Dream,50.9,19.1,196,3550,MALE +Chinstrap,Dream,45.5,17,196,3500,FEMALE +Chinstrap,Dream,50.9,17.9,196,3675,FEMALE +Chinstrap,Dream,50.8,18.5,201,4450,MALE +Chinstrap,Dream,50.1,17.9,190,3400,FEMALE +Chinstrap,Dream,49,19.6,212,4300,MALE +Chinstrap,Dream,51.5,18.7,187,3250,MALE +Chinstrap,Dream,49.8,17.3,198,3675,FEMALE +Chinstrap,Dream,48.1,16.4,199,3325,FEMALE +Chinstrap,Dream,51.4,19,201,3950,MALE +Chinstrap,Dream,45.7,17.3,193,3600,FEMALE +Chinstrap,Dream,50.7,19.7,203,4050,MALE +Chinstrap,Dream,42.5,17.3,187,3350,FEMALE +Chinstrap,Dream,52.2,18.8,197,3450,MALE +Chinstrap,Dream,45.2,16.6,191,3250,FEMALE +Chinstrap,Dream,49.3,19.9,203,4050,MALE +Chinstrap,Dream,50.2,18.8,202,3800,MALE +Chinstrap,Dream,45.6,19.4,194,3525,FEMALE +Chinstrap,Dream,51.9,19.5,206,3950,MALE +Chinstrap,Dream,46.8,16.5,189,3650,FEMALE +Chinstrap,Dream,45.7,17,195,3650,FEMALE +Chinstrap,Dream,55.8,19.8,207,4000,MALE +Chinstrap,Dream,43.5,18.1,202,3400,FEMALE +Chinstrap,Dream,49.6,18.2,193,3775,MALE +Chinstrap,Dream,50.8,19,210,4100,MALE +Chinstrap,Dream,50.2,18.7,198,3775,FEMALE +Gentoo,Biscoe,46.1,13.2,211,4500,FEMALE +Gentoo,Biscoe,50,16.3,230,5700,MALE +Gentoo,Biscoe,48.7,14.1,210,4450,FEMALE +Gentoo,Biscoe,50,15.2,218,5700,MALE +Gentoo,Biscoe,47.6,14.5,215,5400,MALE +Gentoo,Biscoe,46.5,13.5,210,4550,FEMALE +Gentoo,Biscoe,45.4,14.6,211,4800,FEMALE +Gentoo,Biscoe,46.7,15.3,219,5200,MALE +Gentoo,Biscoe,43.3,13.4,209,4400,FEMALE +Gentoo,Biscoe,46.8,15.4,215,5150,MALE +Gentoo,Biscoe,40.9,13.7,214,4650,FEMALE +Gentoo,Biscoe,49,16.1,216,5550,MALE +Gentoo,Biscoe,45.5,13.7,214,4650,FEMALE +Gentoo,Biscoe,48.4,14.6,213,5850,MALE +Gentoo,Biscoe,45.8,14.6,210,4200,FEMALE +Gentoo,Biscoe,49.3,15.7,217,5850,MALE +Gentoo,Biscoe,42,13.5,210,4150,FEMALE +Gentoo,Biscoe,49.2,15.2,221,6300,MALE +Gentoo,Biscoe,46.2,14.5,209,4800,FEMALE +Gentoo,Biscoe,48.7,15.1,222,5350,MALE +Gentoo,Biscoe,50.2,14.3,218,5700,MALE +Gentoo,Biscoe,45.1,14.5,215,5000,FEMALE +Gentoo,Biscoe,46.5,14.5,213,4400,FEMALE +Gentoo,Biscoe,46.3,15.8,215,5050,MALE +Gentoo,Biscoe,42.9,13.1,215,5000,FEMALE +Gentoo,Biscoe,46.1,15.1,215,5100,MALE +Gentoo,Biscoe,44.5,14.3,216,4100, +Gentoo,Biscoe,47.8,15,215,5650,MALE +Gentoo,Biscoe,48.2,14.3,210,4600,FEMALE +Gentoo,Biscoe,50,15.3,220,5550,MALE +Gentoo,Biscoe,47.3,15.3,222,5250,MALE +Gentoo,Biscoe,42.8,14.2,209,4700,FEMALE +Gentoo,Biscoe,45.1,14.5,207,5050,FEMALE +Gentoo,Biscoe,59.6,17,230,6050,MALE +Gentoo,Biscoe,49.1,14.8,220,5150,FEMALE +Gentoo,Biscoe,48.4,16.3,220,5400,MALE +Gentoo,Biscoe,42.6,13.7,213,4950,FEMALE +Gentoo,Biscoe,44.4,17.3,219,5250,MALE +Gentoo,Biscoe,44,13.6,208,4350,FEMALE +Gentoo,Biscoe,48.7,15.7,208,5350,MALE +Gentoo,Biscoe,42.7,13.7,208,3950,FEMALE +Gentoo,Biscoe,49.6,16,225,5700,MALE +Gentoo,Biscoe,45.3,13.7,210,4300,FEMALE +Gentoo,Biscoe,49.6,15,216,4750,MALE +Gentoo,Biscoe,50.5,15.9,222,5550,MALE +Gentoo,Biscoe,43.6,13.9,217,4900,FEMALE +Gentoo,Biscoe,45.5,13.9,210,4200,FEMALE +Gentoo,Biscoe,50.5,15.9,225,5400,MALE +Gentoo,Biscoe,44.9,13.3,213,5100,FEMALE +Gentoo,Biscoe,45.2,15.8,215,5300,MALE +Gentoo,Biscoe,46.6,14.2,210,4850,FEMALE +Gentoo,Biscoe,48.5,14.1,220,5300,MALE +Gentoo,Biscoe,45.1,14.4,210,4400,FEMALE +Gentoo,Biscoe,50.1,15,225,5000,MALE +Gentoo,Biscoe,46.5,14.4,217,4900,FEMALE +Gentoo,Biscoe,45,15.4,220,5050,MALE +Gentoo,Biscoe,43.8,13.9,208,4300,FEMALE +Gentoo,Biscoe,45.5,15,220,5000,MALE +Gentoo,Biscoe,43.2,14.5,208,4450,FEMALE +Gentoo,Biscoe,50.4,15.3,224,5550,MALE +Gentoo,Biscoe,45.3,13.8,208,4200,FEMALE +Gentoo,Biscoe,46.2,14.9,221,5300,MALE +Gentoo,Biscoe,45.7,13.9,214,4400,FEMALE +Gentoo,Biscoe,54.3,15.7,231,5650,MALE +Gentoo,Biscoe,45.8,14.2,219,4700,FEMALE +Gentoo,Biscoe,49.8,16.8,230,5700,MALE +Gentoo,Biscoe,46.2,14.4,214,4650, +Gentoo,Biscoe,49.5,16.2,229,5800,MALE +Gentoo,Biscoe,43.5,14.2,220,4700,FEMALE +Gentoo,Biscoe,50.7,15,223,5550,MALE +Gentoo,Biscoe,47.7,15,216,4750,FEMALE +Gentoo,Biscoe,46.4,15.6,221,5000,MALE +Gentoo,Biscoe,48.2,15.6,221,5100,MALE +Gentoo,Biscoe,46.5,14.8,217,5200,FEMALE +Gentoo,Biscoe,46.4,15,216,4700,FEMALE +Gentoo,Biscoe,48.6,16,230,5800,MALE +Gentoo,Biscoe,47.5,14.2,209,4600,FEMALE +Gentoo,Biscoe,51.1,16.3,220,6000,MALE +Gentoo,Biscoe,45.2,13.8,215,4750,FEMALE +Gentoo,Biscoe,45.2,16.4,223,5950,MALE +Gentoo,Biscoe,49.1,14.5,212,4625,FEMALE +Gentoo,Biscoe,52.5,15.6,221,5450,MALE +Gentoo,Biscoe,47.4,14.6,212,4725,FEMALE +Gentoo,Biscoe,50,15.9,224,5350,MALE +Gentoo,Biscoe,44.9,13.8,212,4750,FEMALE +Gentoo,Biscoe,50.8,17.3,228,5600,MALE +Gentoo,Biscoe,43.4,14.4,218,4600,FEMALE +Gentoo,Biscoe,51.3,14.2,218,5300,MALE +Gentoo,Biscoe,47.5,14,212,4875,FEMALE +Gentoo,Biscoe,52.1,17,230,5550,MALE +Gentoo,Biscoe,47.5,15,218,4950,FEMALE +Gentoo,Biscoe,52.2,17.1,228,5400,MALE +Gentoo,Biscoe,45.5,14.5,212,4750,FEMALE +Gentoo,Biscoe,49.5,16.1,224,5650,MALE +Gentoo,Biscoe,44.5,14.7,214,4850,FEMALE +Gentoo,Biscoe,50.8,15.7,226,5200,MALE +Gentoo,Biscoe,49.4,15.8,216,4925,MALE +Gentoo,Biscoe,46.9,14.6,222,4875,FEMALE +Gentoo,Biscoe,48.4,14.4,203,4625,FEMALE +Gentoo,Biscoe,51.1,16.5,225,5250,MALE +Gentoo,Biscoe,48.5,15,219,4850,FEMALE +Gentoo,Biscoe,55.9,17,228,5600,MALE +Gentoo,Biscoe,47.2,15.5,215,4975,FEMALE +Gentoo,Biscoe,49.1,15,228,5500,MALE +Gentoo,Biscoe,47.3,13.8,216,4725, +Gentoo,Biscoe,46.8,16.1,215,5500,MALE +Gentoo,Biscoe,41.7,14.7,210,4700,FEMALE +Gentoo,Biscoe,53.4,15.8,219,5500,MALE +Gentoo,Biscoe,43.3,14,208,4575,FEMALE +Gentoo,Biscoe,48.1,15.1,209,5500,MALE +Gentoo,Biscoe,50.5,15.2,216,5000,FEMALE +Gentoo,Biscoe,49.8,15.9,229,5950,MALE +Gentoo,Biscoe,43.5,15.2,213,4650,FEMALE +Gentoo,Biscoe,51.5,16.3,230,5500,MALE +Gentoo,Biscoe,46.2,14.1,217,4375,FEMALE +Gentoo,Biscoe,55.1,16,230,5850,MALE +Gentoo,Biscoe,44.5,15.7,217,4875, +Gentoo,Biscoe,48.8,16.2,222,6000,MALE +Gentoo,Biscoe,47.2,13.7,214,4925,FEMALE +Gentoo,Biscoe,NaN,NaN,NaN,NaN, +Gentoo,Biscoe,46.8,14.3,215,4850,FEMALE +Gentoo,Biscoe,50.4,15.7,222,5750,MALE +Gentoo,Biscoe,45.2,14.8,212,5200,FEMALE +Gentoo,Biscoe,49.9,16.1,213,5400,MALE \ No newline at end of file diff --git a/src/content/tutorial/01-basics/05-axes-grids/+assets/src/routes/+error.svelte b/src/content/tutorial/01-basics/05-axes-grids/+assets/src/routes/+error.svelte new file mode 100644 index 000000000..6706246e4 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/+assets/src/routes/+error.svelte @@ -0,0 +1,25 @@ + + +{#if page.status === 404} +

    Not found

    +

    + Go to / +

    +{:else} +

    + Server-side rendering failed with HTTP status code + {page.status} +

    +{/if} + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/+assets/src/routes/+layout.js b/src/content/tutorial/01-basics/05-axes-grids/+assets/src/routes/+layout.js new file mode 100644 index 000000000..a3d15781a --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/+assets/src/routes/+layout.js @@ -0,0 +1 @@ +export const ssr = false; diff --git a/src/content/tutorial/01-basics/05-axes-grids/+assets/src/routes/+page.svelte b/src/content/tutorial/01-basics/05-axes-grids/+assets/src/routes/+page.svelte new file mode 100644 index 000000000..3736ee076 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/+assets/src/routes/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/01-implicit-marks/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/05-axes-grids/01-implicit-marks/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..3ecb665b5 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/01-implicit-marks/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/01-implicit-marks/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/05-axes-grids/01-implicit-marks/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..b22403e7c --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/01-implicit-marks/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/01-implicit-marks/index.md b/src/content/tutorial/01-basics/05-axes-grids/01-implicit-marks/index.md new file mode 100644 index 000000000..c2de94047 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/01-implicit-marks/index.md @@ -0,0 +1,15 @@ +--- +title: Implicit axes +--- + +You already noticed that SveltePlot adds **axes** to your plot automatically — they are _implicit_ marks that appear without you adding them explicitly. You can disable the implicit axes entirely with `axes={false}`. + +```svelte + +``` + +Two more implicit marks are available as shorthand props on ``: `grid` adds grid lines, and `frame` adds a border around the plot area: + +```svelte + +``` diff --git a/src/content/tutorial/01-basics/05-axes-grids/02-explicit-axes/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/05-axes-grids/02-explicit-axes/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..5af223bc8 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/02-explicit-axes/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/02-explicit-axes/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/05-axes-grids/02-explicit-axes/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..81ca1f78e --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/02-explicit-axes/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/02-explicit-axes/index.md b/src/content/tutorial/01-basics/05-axes-grids/02-explicit-axes/index.md new file mode 100644 index 000000000..061c2a500 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/02-explicit-axes/index.md @@ -0,0 +1,38 @@ +--- +title: Explicit axes +--- + +The implicit axes are just `AxisX` and `AxisY` marks that SveltePlot inserts for you. Add them explicitly to access their full set of options. + +Import them and add them to the plot, turning off the implicit ones with `axes={false}`: + +```js +import { + Plot, + Dot+++,+++ ++ AxisX, ++ AxisY +} from 'svelteplot'; +``` + +```svelte + + + ++++++ + ++++++ + +``` + +SveltePlot detects the explicit axes and skips its own, so you can drop `axes={false}`: + +```svelte +------ +++++++ +``` + +Explicit axes expose props like `label`, `ticks`, `tickFormat`, and `anchor` for fine-grained control. + +```svelte + + +``` diff --git a/src/content/tutorial/01-basics/05-axes-grids/03-implicit-grids/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/05-axes-grids/03-implicit-grids/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..3ecb665b5 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/03-implicit-grids/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/03-implicit-grids/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/05-axes-grids/03-implicit-grids/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..5ac6d3445 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/03-implicit-grids/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/03-implicit-grids/index.md b/src/content/tutorial/01-basics/05-axes-grids/03-implicit-grids/index.md new file mode 100644 index 000000000..a95ba1f36 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/03-implicit-grids/index.md @@ -0,0 +1,16 @@ +--- +title: Implicit grids +--- + +The `grid` prop on `` adds grid lines for both axes at once. + +```svelte + +``` + +You can also control them per axis using scale options: + +```svelte +- ++ +``` diff --git a/src/content/tutorial/01-basics/05-axes-grids/04-explicit-grids/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/05-axes-grids/04-explicit-grids/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..3ecb665b5 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/04-explicit-grids/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/04-explicit-grids/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/05-axes-grids/04-explicit-grids/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..a5afef69a --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/04-explicit-grids/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/content/tutorial/01-basics/05-axes-grids/04-explicit-grids/index.md b/src/content/tutorial/01-basics/05-axes-grids/04-explicit-grids/index.md new file mode 100644 index 000000000..e952250a8 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/04-explicit-grids/index.md @@ -0,0 +1,33 @@ +--- +title: Explicit grids +--- + +The implicit grid is just `GridX` and `GridY` marks that SveltePlot inserts for you. Import them directly to unlock their full set of style props. + +Replace the scale option with explicit marks: + +```js +import { + Plot, + Dot+++,+++ ++ GridY +} from 'svelteplot'; +``` + +```svelte + + ++++++ + +``` + +Now customise the appearance — dashed lines on the y grid, reduced opacity on the x grid: + +```svelte +------ ++ +``` + +Any SVG stroke prop works: `stroke`, `strokeOpacity`, `strokeWidth`, `strokeDasharray`. Grid marks also accept a `data` prop to draw lines at specific values instead of the auto-computed tick positions. diff --git a/src/content/tutorial/01-basics/05-axes-grids/index.md b/src/content/tutorial/01-basics/05-axes-grids/index.md new file mode 100644 index 000000000..c61d40371 --- /dev/null +++ b/src/content/tutorial/01-basics/05-axes-grids/index.md @@ -0,0 +1,5 @@ +--- +title: Axes and grids +scope: { 'prefix': '/src/lib/', 'name': 'src' } +focus: /src/lib/App.svelte +--- diff --git a/src/content/tutorial/01-basics/06-faceting/+assets/src/lib/penguins.csv b/src/content/tutorial/01-basics/06-faceting/+assets/src/lib/penguins.csv new file mode 100644 index 000000000..83f32630b --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/+assets/src/lib/penguins.csv @@ -0,0 +1,345 @@ +species,island,bill_length_mm,bill_depth_mm,flipper_length_mm,body_mass_g,sex +Adelie,Torgersen,39.1,18.7,181,3750,MALE +Adelie,Torgersen,39.5,17.4,186,3800,FEMALE +Adelie,Torgersen,40.3,18,195,3250,FEMALE +Adelie,Torgersen,NaN,NaN,NaN,NaN, +Adelie,Torgersen,36.7,19.3,193,3450,FEMALE +Adelie,Torgersen,39.3,20.6,190,3650,MALE +Adelie,Torgersen,38.9,17.8,181,3625,FEMALE +Adelie,Torgersen,39.2,19.6,195,4675,MALE +Adelie,Torgersen,34.1,18.1,193,3475, +Adelie,Torgersen,42,20.2,190,4250, +Adelie,Torgersen,37.8,17.1,186,3300, +Adelie,Torgersen,37.8,17.3,180,3700, +Adelie,Torgersen,41.1,17.6,182,3200,FEMALE +Adelie,Torgersen,38.6,21.2,191,3800,MALE +Adelie,Torgersen,34.6,21.1,198,4400,MALE +Adelie,Torgersen,36.6,17.8,185,3700,FEMALE +Adelie,Torgersen,38.7,19,195,3450,FEMALE +Adelie,Torgersen,42.5,20.7,197,4500,MALE +Adelie,Torgersen,34.4,18.4,184,3325,FEMALE +Adelie,Torgersen,46,21.5,194,4200,MALE +Adelie,Biscoe,37.8,18.3,174,3400,FEMALE +Adelie,Biscoe,37.7,18.7,180,3600,MALE +Adelie,Biscoe,35.9,19.2,189,3800,FEMALE +Adelie,Biscoe,38.2,18.1,185,3950,MALE +Adelie,Biscoe,38.8,17.2,180,3800,MALE +Adelie,Biscoe,35.3,18.9,187,3800,FEMALE +Adelie,Biscoe,40.6,18.6,183,3550,MALE +Adelie,Biscoe,40.5,17.9,187,3200,FEMALE +Adelie,Biscoe,37.9,18.6,172,3150,FEMALE +Adelie,Biscoe,40.5,18.9,180,3950,MALE +Adelie,Dream,39.5,16.7,178,3250,FEMALE +Adelie,Dream,37.2,18.1,178,3900,MALE +Adelie,Dream,39.5,17.8,188,3300,FEMALE +Adelie,Dream,40.9,18.9,184,3900,MALE +Adelie,Dream,36.4,17,195,3325,FEMALE +Adelie,Dream,39.2,21.1,196,4150,MALE +Adelie,Dream,38.8,20,190,3950,MALE +Adelie,Dream,42.2,18.5,180,3550,FEMALE +Adelie,Dream,37.6,19.3,181,3300,FEMALE +Adelie,Dream,39.8,19.1,184,4650,MALE +Adelie,Dream,36.5,18,182,3150,FEMALE +Adelie,Dream,40.8,18.4,195,3900,MALE +Adelie,Dream,36,18.5,186,3100,FEMALE +Adelie,Dream,44.1,19.7,196,4400,MALE +Adelie,Dream,37,16.9,185,3000,FEMALE +Adelie,Dream,39.6,18.8,190,4600,MALE +Adelie,Dream,41.1,19,182,3425,MALE +Adelie,Dream,37.5,18.9,179,2975, +Adelie,Dream,36,17.9,190,3450,FEMALE +Adelie,Dream,42.3,21.2,191,4150,MALE +Adelie,Biscoe,39.6,17.7,186,3500,FEMALE +Adelie,Biscoe,40.1,18.9,188,4300,MALE +Adelie,Biscoe,35,17.9,190,3450,FEMALE +Adelie,Biscoe,42,19.5,200,4050,MALE +Adelie,Biscoe,34.5,18.1,187,2900,FEMALE +Adelie,Biscoe,41.4,18.6,191,3700,MALE +Adelie,Biscoe,39,17.5,186,3550,FEMALE +Adelie,Biscoe,40.6,18.8,193,3800,MALE +Adelie,Biscoe,36.5,16.6,181,2850,FEMALE +Adelie,Biscoe,37.6,19.1,194,3750,MALE +Adelie,Biscoe,35.7,16.9,185,3150,FEMALE +Adelie,Biscoe,41.3,21.1,195,4400,MALE +Adelie,Biscoe,37.6,17,185,3600,FEMALE +Adelie,Biscoe,41.1,18.2,192,4050,MALE +Adelie,Biscoe,36.4,17.1,184,2850,FEMALE +Adelie,Biscoe,41.6,18,192,3950,MALE +Adelie,Biscoe,35.5,16.2,195,3350,FEMALE +Adelie,Biscoe,41.1,19.1,188,4100,MALE +Adelie,Torgersen,35.9,16.6,190,3050,FEMALE +Adelie,Torgersen,41.8,19.4,198,4450,MALE +Adelie,Torgersen,33.5,19,190,3600,FEMALE +Adelie,Torgersen,39.7,18.4,190,3900,MALE +Adelie,Torgersen,39.6,17.2,196,3550,FEMALE +Adelie,Torgersen,45.8,18.9,197,4150,MALE +Adelie,Torgersen,35.5,17.5,190,3700,FEMALE +Adelie,Torgersen,42.8,18.5,195,4250,MALE +Adelie,Torgersen,40.9,16.8,191,3700,FEMALE +Adelie,Torgersen,37.2,19.4,184,3900,MALE +Adelie,Torgersen,36.2,16.1,187,3550,FEMALE +Adelie,Torgersen,42.1,19.1,195,4000,MALE +Adelie,Torgersen,34.6,17.2,189,3200,FEMALE +Adelie,Torgersen,42.9,17.6,196,4700,MALE +Adelie,Torgersen,36.7,18.8,187,3800,FEMALE +Adelie,Torgersen,35.1,19.4,193,4200,MALE +Adelie,Dream,37.3,17.8,191,3350,FEMALE +Adelie,Dream,41.3,20.3,194,3550,MALE +Adelie,Dream,36.3,19.5,190,3800,MALE +Adelie,Dream,36.9,18.6,189,3500,FEMALE +Adelie,Dream,38.3,19.2,189,3950,MALE +Adelie,Dream,38.9,18.8,190,3600,FEMALE +Adelie,Dream,35.7,18,202,3550,FEMALE +Adelie,Dream,41.1,18.1,205,4300,MALE +Adelie,Dream,34,17.1,185,3400,FEMALE +Adelie,Dream,39.6,18.1,186,4450,MALE +Adelie,Dream,36.2,17.3,187,3300,FEMALE +Adelie,Dream,40.8,18.9,208,4300,MALE +Adelie,Dream,38.1,18.6,190,3700,FEMALE +Adelie,Dream,40.3,18.5,196,4350,MALE +Adelie,Dream,33.1,16.1,178,2900,FEMALE +Adelie,Dream,43.2,18.5,192,4100,MALE +Adelie,Biscoe,35,17.9,192,3725,FEMALE +Adelie,Biscoe,41,20,203,4725,MALE +Adelie,Biscoe,37.7,16,183,3075,FEMALE +Adelie,Biscoe,37.8,20,190,4250,MALE +Adelie,Biscoe,37.9,18.6,193,2925,FEMALE +Adelie,Biscoe,39.7,18.9,184,3550,MALE +Adelie,Biscoe,38.6,17.2,199,3750,FEMALE +Adelie,Biscoe,38.2,20,190,3900,MALE +Adelie,Biscoe,38.1,17,181,3175,FEMALE +Adelie,Biscoe,43.2,19,197,4775,MALE +Adelie,Biscoe,38.1,16.5,198,3825,FEMALE +Adelie,Biscoe,45.6,20.3,191,4600,MALE +Adelie,Biscoe,39.7,17.7,193,3200,FEMALE +Adelie,Biscoe,42.2,19.5,197,4275,MALE +Adelie,Biscoe,39.6,20.7,191,3900,FEMALE +Adelie,Biscoe,42.7,18.3,196,4075,MALE +Adelie,Torgersen,38.6,17,188,2900,FEMALE +Adelie,Torgersen,37.3,20.5,199,3775,MALE +Adelie,Torgersen,35.7,17,189,3350,FEMALE +Adelie,Torgersen,41.1,18.6,189,3325,MALE +Adelie,Torgersen,36.2,17.2,187,3150,FEMALE +Adelie,Torgersen,37.7,19.8,198,3500,MALE +Adelie,Torgersen,40.2,17,176,3450,FEMALE +Adelie,Torgersen,41.4,18.5,202,3875,MALE +Adelie,Torgersen,35.2,15.9,186,3050,FEMALE +Adelie,Torgersen,40.6,19,199,4000,MALE +Adelie,Torgersen,38.8,17.6,191,3275,FEMALE +Adelie,Torgersen,41.5,18.3,195,4300,MALE +Adelie,Torgersen,39,17.1,191,3050,FEMALE +Adelie,Torgersen,44.1,18,210,4000,MALE +Adelie,Torgersen,38.5,17.9,190,3325,FEMALE +Adelie,Torgersen,43.1,19.2,197,3500,MALE +Adelie,Dream,36.8,18.5,193,3500,FEMALE +Adelie,Dream,37.5,18.5,199,4475,MALE +Adelie,Dream,38.1,17.6,187,3425,FEMALE +Adelie,Dream,41.1,17.5,190,3900,MALE +Adelie,Dream,35.6,17.5,191,3175,FEMALE +Adelie,Dream,40.2,20.1,200,3975,MALE +Adelie,Dream,37,16.5,185,3400,FEMALE +Adelie,Dream,39.7,17.9,193,4250,MALE +Adelie,Dream,40.2,17.1,193,3400,FEMALE +Adelie,Dream,40.6,17.2,187,3475,MALE +Adelie,Dream,32.1,15.5,188,3050,FEMALE +Adelie,Dream,40.7,17,190,3725,MALE +Adelie,Dream,37.3,16.8,192,3000,FEMALE +Adelie,Dream,39,18.7,185,3650,MALE +Adelie,Dream,39.2,18.6,190,4250,MALE +Adelie,Dream,36.6,18.4,184,3475,FEMALE +Adelie,Dream,36,17.8,195,3450,FEMALE +Adelie,Dream,37.8,18.1,193,3750,MALE +Adelie,Dream,36,17.1,187,3700,FEMALE +Adelie,Dream,41.5,18.5,201,4000,MALE +Chinstrap,Dream,46.5,17.9,192,3500,FEMALE +Chinstrap,Dream,50,19.5,196,3900,MALE +Chinstrap,Dream,51.3,19.2,193,3650,MALE +Chinstrap,Dream,45.4,18.7,188,3525,FEMALE +Chinstrap,Dream,52.7,19.8,197,3725,MALE +Chinstrap,Dream,45.2,17.8,198,3950,FEMALE +Chinstrap,Dream,46.1,18.2,178,3250,FEMALE +Chinstrap,Dream,51.3,18.2,197,3750,MALE +Chinstrap,Dream,46,18.9,195,4150,FEMALE +Chinstrap,Dream,51.3,19.9,198,3700,MALE +Chinstrap,Dream,46.6,17.8,193,3800,FEMALE +Chinstrap,Dream,51.7,20.3,194,3775,MALE +Chinstrap,Dream,47,17.3,185,3700,FEMALE +Chinstrap,Dream,52,18.1,201,4050,MALE +Chinstrap,Dream,45.9,17.1,190,3575,FEMALE +Chinstrap,Dream,50.5,19.6,201,4050,MALE +Chinstrap,Dream,50.3,20,197,3300,MALE +Chinstrap,Dream,58,17.8,181,3700,FEMALE +Chinstrap,Dream,46.4,18.6,190,3450,FEMALE +Chinstrap,Dream,49.2,18.2,195,4400,MALE +Chinstrap,Dream,42.4,17.3,181,3600,FEMALE +Chinstrap,Dream,48.5,17.5,191,3400,MALE +Chinstrap,Dream,43.2,16.6,187,2900,FEMALE +Chinstrap,Dream,50.6,19.4,193,3800,MALE +Chinstrap,Dream,46.7,17.9,195,3300,FEMALE +Chinstrap,Dream,52,19,197,4150,MALE +Chinstrap,Dream,50.5,18.4,200,3400,FEMALE +Chinstrap,Dream,49.5,19,200,3800,MALE +Chinstrap,Dream,46.4,17.8,191,3700,FEMALE +Chinstrap,Dream,52.8,20,205,4550,MALE +Chinstrap,Dream,40.9,16.6,187,3200,FEMALE +Chinstrap,Dream,54.2,20.8,201,4300,MALE +Chinstrap,Dream,42.5,16.7,187,3350,FEMALE +Chinstrap,Dream,51,18.8,203,4100,MALE +Chinstrap,Dream,49.7,18.6,195,3600,MALE +Chinstrap,Dream,47.5,16.8,199,3900,FEMALE +Chinstrap,Dream,47.6,18.3,195,3850,FEMALE +Chinstrap,Dream,52,20.7,210,4800,MALE +Chinstrap,Dream,46.9,16.6,192,2700,FEMALE +Chinstrap,Dream,53.5,19.9,205,4500,MALE +Chinstrap,Dream,49,19.5,210,3950,MALE +Chinstrap,Dream,46.2,17.5,187,3650,FEMALE +Chinstrap,Dream,50.9,19.1,196,3550,MALE +Chinstrap,Dream,45.5,17,196,3500,FEMALE +Chinstrap,Dream,50.9,17.9,196,3675,FEMALE +Chinstrap,Dream,50.8,18.5,201,4450,MALE +Chinstrap,Dream,50.1,17.9,190,3400,FEMALE +Chinstrap,Dream,49,19.6,212,4300,MALE +Chinstrap,Dream,51.5,18.7,187,3250,MALE +Chinstrap,Dream,49.8,17.3,198,3675,FEMALE +Chinstrap,Dream,48.1,16.4,199,3325,FEMALE +Chinstrap,Dream,51.4,19,201,3950,MALE +Chinstrap,Dream,45.7,17.3,193,3600,FEMALE +Chinstrap,Dream,50.7,19.7,203,4050,MALE +Chinstrap,Dream,42.5,17.3,187,3350,FEMALE +Chinstrap,Dream,52.2,18.8,197,3450,MALE +Chinstrap,Dream,45.2,16.6,191,3250,FEMALE +Chinstrap,Dream,49.3,19.9,203,4050,MALE +Chinstrap,Dream,50.2,18.8,202,3800,MALE +Chinstrap,Dream,45.6,19.4,194,3525,FEMALE +Chinstrap,Dream,51.9,19.5,206,3950,MALE +Chinstrap,Dream,46.8,16.5,189,3650,FEMALE +Chinstrap,Dream,45.7,17,195,3650,FEMALE +Chinstrap,Dream,55.8,19.8,207,4000,MALE +Chinstrap,Dream,43.5,18.1,202,3400,FEMALE +Chinstrap,Dream,49.6,18.2,193,3775,MALE +Chinstrap,Dream,50.8,19,210,4100,MALE +Chinstrap,Dream,50.2,18.7,198,3775,FEMALE +Gentoo,Biscoe,46.1,13.2,211,4500,FEMALE +Gentoo,Biscoe,50,16.3,230,5700,MALE +Gentoo,Biscoe,48.7,14.1,210,4450,FEMALE +Gentoo,Biscoe,50,15.2,218,5700,MALE +Gentoo,Biscoe,47.6,14.5,215,5400,MALE +Gentoo,Biscoe,46.5,13.5,210,4550,FEMALE +Gentoo,Biscoe,45.4,14.6,211,4800,FEMALE +Gentoo,Biscoe,46.7,15.3,219,5200,MALE +Gentoo,Biscoe,43.3,13.4,209,4400,FEMALE +Gentoo,Biscoe,46.8,15.4,215,5150,MALE +Gentoo,Biscoe,40.9,13.7,214,4650,FEMALE +Gentoo,Biscoe,49,16.1,216,5550,MALE +Gentoo,Biscoe,45.5,13.7,214,4650,FEMALE +Gentoo,Biscoe,48.4,14.6,213,5850,MALE +Gentoo,Biscoe,45.8,14.6,210,4200,FEMALE +Gentoo,Biscoe,49.3,15.7,217,5850,MALE +Gentoo,Biscoe,42,13.5,210,4150,FEMALE +Gentoo,Biscoe,49.2,15.2,221,6300,MALE +Gentoo,Biscoe,46.2,14.5,209,4800,FEMALE +Gentoo,Biscoe,48.7,15.1,222,5350,MALE +Gentoo,Biscoe,50.2,14.3,218,5700,MALE +Gentoo,Biscoe,45.1,14.5,215,5000,FEMALE +Gentoo,Biscoe,46.5,14.5,213,4400,FEMALE +Gentoo,Biscoe,46.3,15.8,215,5050,MALE +Gentoo,Biscoe,42.9,13.1,215,5000,FEMALE +Gentoo,Biscoe,46.1,15.1,215,5100,MALE +Gentoo,Biscoe,44.5,14.3,216,4100, +Gentoo,Biscoe,47.8,15,215,5650,MALE +Gentoo,Biscoe,48.2,14.3,210,4600,FEMALE +Gentoo,Biscoe,50,15.3,220,5550,MALE +Gentoo,Biscoe,47.3,15.3,222,5250,MALE +Gentoo,Biscoe,42.8,14.2,209,4700,FEMALE +Gentoo,Biscoe,45.1,14.5,207,5050,FEMALE +Gentoo,Biscoe,59.6,17,230,6050,MALE +Gentoo,Biscoe,49.1,14.8,220,5150,FEMALE +Gentoo,Biscoe,48.4,16.3,220,5400,MALE +Gentoo,Biscoe,42.6,13.7,213,4950,FEMALE +Gentoo,Biscoe,44.4,17.3,219,5250,MALE +Gentoo,Biscoe,44,13.6,208,4350,FEMALE +Gentoo,Biscoe,48.7,15.7,208,5350,MALE +Gentoo,Biscoe,42.7,13.7,208,3950,FEMALE +Gentoo,Biscoe,49.6,16,225,5700,MALE +Gentoo,Biscoe,45.3,13.7,210,4300,FEMALE +Gentoo,Biscoe,49.6,15,216,4750,MALE +Gentoo,Biscoe,50.5,15.9,222,5550,MALE +Gentoo,Biscoe,43.6,13.9,217,4900,FEMALE +Gentoo,Biscoe,45.5,13.9,210,4200,FEMALE +Gentoo,Biscoe,50.5,15.9,225,5400,MALE +Gentoo,Biscoe,44.9,13.3,213,5100,FEMALE +Gentoo,Biscoe,45.2,15.8,215,5300,MALE +Gentoo,Biscoe,46.6,14.2,210,4850,FEMALE +Gentoo,Biscoe,48.5,14.1,220,5300,MALE +Gentoo,Biscoe,45.1,14.4,210,4400,FEMALE +Gentoo,Biscoe,50.1,15,225,5000,MALE +Gentoo,Biscoe,46.5,14.4,217,4900,FEMALE +Gentoo,Biscoe,45,15.4,220,5050,MALE +Gentoo,Biscoe,43.8,13.9,208,4300,FEMALE +Gentoo,Biscoe,45.5,15,220,5000,MALE +Gentoo,Biscoe,43.2,14.5,208,4450,FEMALE +Gentoo,Biscoe,50.4,15.3,224,5550,MALE +Gentoo,Biscoe,45.3,13.8,208,4200,FEMALE +Gentoo,Biscoe,46.2,14.9,221,5300,MALE +Gentoo,Biscoe,45.7,13.9,214,4400,FEMALE +Gentoo,Biscoe,54.3,15.7,231,5650,MALE +Gentoo,Biscoe,45.8,14.2,219,4700,FEMALE +Gentoo,Biscoe,49.8,16.8,230,5700,MALE +Gentoo,Biscoe,46.2,14.4,214,4650, +Gentoo,Biscoe,49.5,16.2,229,5800,MALE +Gentoo,Biscoe,43.5,14.2,220,4700,FEMALE +Gentoo,Biscoe,50.7,15,223,5550,MALE +Gentoo,Biscoe,47.7,15,216,4750,FEMALE +Gentoo,Biscoe,46.4,15.6,221,5000,MALE +Gentoo,Biscoe,48.2,15.6,221,5100,MALE +Gentoo,Biscoe,46.5,14.8,217,5200,FEMALE +Gentoo,Biscoe,46.4,15,216,4700,FEMALE +Gentoo,Biscoe,48.6,16,230,5800,MALE +Gentoo,Biscoe,47.5,14.2,209,4600,FEMALE +Gentoo,Biscoe,51.1,16.3,220,6000,MALE +Gentoo,Biscoe,45.2,13.8,215,4750,FEMALE +Gentoo,Biscoe,45.2,16.4,223,5950,MALE +Gentoo,Biscoe,49.1,14.5,212,4625,FEMALE +Gentoo,Biscoe,52.5,15.6,221,5450,MALE +Gentoo,Biscoe,47.4,14.6,212,4725,FEMALE +Gentoo,Biscoe,50,15.9,224,5350,MALE +Gentoo,Biscoe,44.9,13.8,212,4750,FEMALE +Gentoo,Biscoe,50.8,17.3,228,5600,MALE +Gentoo,Biscoe,43.4,14.4,218,4600,FEMALE +Gentoo,Biscoe,51.3,14.2,218,5300,MALE +Gentoo,Biscoe,47.5,14,212,4875,FEMALE +Gentoo,Biscoe,52.1,17,230,5550,MALE +Gentoo,Biscoe,47.5,15,218,4950,FEMALE +Gentoo,Biscoe,52.2,17.1,228,5400,MALE +Gentoo,Biscoe,45.5,14.5,212,4750,FEMALE +Gentoo,Biscoe,49.5,16.1,224,5650,MALE +Gentoo,Biscoe,44.5,14.7,214,4850,FEMALE +Gentoo,Biscoe,50.8,15.7,226,5200,MALE +Gentoo,Biscoe,49.4,15.8,216,4925,MALE +Gentoo,Biscoe,46.9,14.6,222,4875,FEMALE +Gentoo,Biscoe,48.4,14.4,203,4625,FEMALE +Gentoo,Biscoe,51.1,16.5,225,5250,MALE +Gentoo,Biscoe,48.5,15,219,4850,FEMALE +Gentoo,Biscoe,55.9,17,228,5600,MALE +Gentoo,Biscoe,47.2,15.5,215,4975,FEMALE +Gentoo,Biscoe,49.1,15,228,5500,MALE +Gentoo,Biscoe,47.3,13.8,216,4725, +Gentoo,Biscoe,46.8,16.1,215,5500,MALE +Gentoo,Biscoe,41.7,14.7,210,4700,FEMALE +Gentoo,Biscoe,53.4,15.8,219,5500,MALE +Gentoo,Biscoe,43.3,14,208,4575,FEMALE +Gentoo,Biscoe,48.1,15.1,209,5500,MALE +Gentoo,Biscoe,50.5,15.2,216,5000,FEMALE +Gentoo,Biscoe,49.8,15.9,229,5950,MALE +Gentoo,Biscoe,43.5,15.2,213,4650,FEMALE +Gentoo,Biscoe,51.5,16.3,230,5500,MALE +Gentoo,Biscoe,46.2,14.1,217,4375,FEMALE +Gentoo,Biscoe,55.1,16,230,5850,MALE +Gentoo,Biscoe,44.5,15.7,217,4875, +Gentoo,Biscoe,48.8,16.2,222,6000,MALE +Gentoo,Biscoe,47.2,13.7,214,4925,FEMALE +Gentoo,Biscoe,NaN,NaN,NaN,NaN, +Gentoo,Biscoe,46.8,14.3,215,4850,FEMALE +Gentoo,Biscoe,50.4,15.7,222,5750,MALE +Gentoo,Biscoe,45.2,14.8,212,5200,FEMALE +Gentoo,Biscoe,49.9,16.1,213,5400,MALE \ No newline at end of file diff --git a/src/content/tutorial/01-basics/06-faceting/+assets/src/routes/+error.svelte b/src/content/tutorial/01-basics/06-faceting/+assets/src/routes/+error.svelte new file mode 100644 index 000000000..6706246e4 --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/+assets/src/routes/+error.svelte @@ -0,0 +1,25 @@ + + +{#if page.status === 404} +

    Not found

    +

    + Go to / +

    +{:else} +

    + Server-side rendering failed with HTTP status code + {page.status} +

    +{/if} + + diff --git a/src/content/tutorial/01-basics/06-faceting/+assets/src/routes/+layout.js b/src/content/tutorial/01-basics/06-faceting/+assets/src/routes/+layout.js new file mode 100644 index 000000000..a3d15781a --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/+assets/src/routes/+layout.js @@ -0,0 +1 @@ +export const ssr = false; diff --git a/src/content/tutorial/01-basics/06-faceting/+assets/src/routes/+page.svelte b/src/content/tutorial/01-basics/06-faceting/+assets/src/routes/+page.svelte new file mode 100644 index 000000000..3736ee076 --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/+assets/src/routes/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/content/tutorial/01-basics/06-faceting/01-faceting/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/06-faceting/01-faceting/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..a37f8daa7 --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/01-faceting/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/src/content/tutorial/01-basics/06-faceting/01-faceting/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/06-faceting/01-faceting/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..4b636f4da --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/01-faceting/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/content/tutorial/01-basics/06-faceting/01-faceting/index.md b/src/content/tutorial/01-basics/06-faceting/01-faceting/index.md new file mode 100644 index 000000000..fa34b06a7 --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/01-faceting/index.md @@ -0,0 +1,27 @@ +--- +title: Faceting +--- + +Our scatter plot colours dots by island — three clusters are already visible. But what if we also want to see how penguins are distributed _across islands_? + +Encoding a second grouping as another channel would make the chart unreadable fast. **Faceting** solves this by splitting the plot into one panel per group. + +Add `fy="island"` to the `` mark to split horizontally by island: + +```svelte + +``` + +SveltePlot creates one sub-plot per unique island value and lines them up side by side. The y-axis is shared across panels so values stay comparable. + +It helps to add a frame around each panel to make them visually distinct. Add `frame` to the `` component, and perhaps add a little bit of `inset`: + +```svelte + +``` diff --git a/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..b90367eec --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..c057eb40f --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,20 @@ + + + + + + diff --git a/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/+assets/lib/App.svelte b/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/+assets/lib/App.svelte new file mode 100644 index 000000000..d850fdae8 --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/+assets/lib/App.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/index.md b/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/index.md new file mode 100644 index 000000000..fd4bc77e9 --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/02-unfaceted-data/index.md @@ -0,0 +1,29 @@ +--- +title: Unfaceted data +--- + +Each panel currently only shows the penguins from that island — the other rows are filtered out. That makes it easy to focus on one island, but harder to compare it against the whole population. + +You can add context to every panel at once by including a mark **without** a facet channel. A mark that has no `fy` (or `fx`) is drawn in every panel using the full dataset. + +Add a second `` before the existing one, without `fy`: + +```svelte + ++ + + +``` + +The gray dots appear in every panel, giving each island's cluster a reference against the full population. diff --git a/src/content/tutorial/01-basics/06-faceting/index.md b/src/content/tutorial/01-basics/06-faceting/index.md new file mode 100644 index 000000000..e78dc081e --- /dev/null +++ b/src/content/tutorial/01-basics/06-faceting/index.md @@ -0,0 +1,5 @@ +--- +title: Faceting +scope: { 'prefix': '/src/lib/', 'name': 'src' } +focus: /src/lib/App.svelte +--- diff --git a/src/content/tutorial/01-basics/index.md b/src/content/tutorial/01-basics/index.md new file mode 100644 index 000000000..9967110a5 --- /dev/null +++ b/src/content/tutorial/01-basics/index.md @@ -0,0 +1,8 @@ +--- +title: SveltePlot +label: Basics +scope: { 'prefix': '/src/lib/', 'name': 'src' } +focus: /src/lib/App.svelte +--- + +This tutorial assumes you are already familiar with Svelte. If not, we recommend taking a quick look at the [Svelte tutorial](https://svelte.dev/tutorial) first. diff --git a/src/content/tutorial/02-advanced/01-tooltips/+assets/src/lib/penguins.csv b/src/content/tutorial/02-advanced/01-tooltips/+assets/src/lib/penguins.csv new file mode 100644 index 000000000..83f32630b --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/+assets/src/lib/penguins.csv @@ -0,0 +1,345 @@ +species,island,bill_length_mm,bill_depth_mm,flipper_length_mm,body_mass_g,sex +Adelie,Torgersen,39.1,18.7,181,3750,MALE +Adelie,Torgersen,39.5,17.4,186,3800,FEMALE +Adelie,Torgersen,40.3,18,195,3250,FEMALE +Adelie,Torgersen,NaN,NaN,NaN,NaN, +Adelie,Torgersen,36.7,19.3,193,3450,FEMALE +Adelie,Torgersen,39.3,20.6,190,3650,MALE +Adelie,Torgersen,38.9,17.8,181,3625,FEMALE +Adelie,Torgersen,39.2,19.6,195,4675,MALE +Adelie,Torgersen,34.1,18.1,193,3475, +Adelie,Torgersen,42,20.2,190,4250, +Adelie,Torgersen,37.8,17.1,186,3300, +Adelie,Torgersen,37.8,17.3,180,3700, +Adelie,Torgersen,41.1,17.6,182,3200,FEMALE +Adelie,Torgersen,38.6,21.2,191,3800,MALE +Adelie,Torgersen,34.6,21.1,198,4400,MALE +Adelie,Torgersen,36.6,17.8,185,3700,FEMALE +Adelie,Torgersen,38.7,19,195,3450,FEMALE +Adelie,Torgersen,42.5,20.7,197,4500,MALE +Adelie,Torgersen,34.4,18.4,184,3325,FEMALE +Adelie,Torgersen,46,21.5,194,4200,MALE +Adelie,Biscoe,37.8,18.3,174,3400,FEMALE +Adelie,Biscoe,37.7,18.7,180,3600,MALE +Adelie,Biscoe,35.9,19.2,189,3800,FEMALE +Adelie,Biscoe,38.2,18.1,185,3950,MALE +Adelie,Biscoe,38.8,17.2,180,3800,MALE +Adelie,Biscoe,35.3,18.9,187,3800,FEMALE +Adelie,Biscoe,40.6,18.6,183,3550,MALE +Adelie,Biscoe,40.5,17.9,187,3200,FEMALE +Adelie,Biscoe,37.9,18.6,172,3150,FEMALE +Adelie,Biscoe,40.5,18.9,180,3950,MALE +Adelie,Dream,39.5,16.7,178,3250,FEMALE +Adelie,Dream,37.2,18.1,178,3900,MALE +Adelie,Dream,39.5,17.8,188,3300,FEMALE +Adelie,Dream,40.9,18.9,184,3900,MALE +Adelie,Dream,36.4,17,195,3325,FEMALE +Adelie,Dream,39.2,21.1,196,4150,MALE +Adelie,Dream,38.8,20,190,3950,MALE +Adelie,Dream,42.2,18.5,180,3550,FEMALE +Adelie,Dream,37.6,19.3,181,3300,FEMALE +Adelie,Dream,39.8,19.1,184,4650,MALE +Adelie,Dream,36.5,18,182,3150,FEMALE +Adelie,Dream,40.8,18.4,195,3900,MALE +Adelie,Dream,36,18.5,186,3100,FEMALE +Adelie,Dream,44.1,19.7,196,4400,MALE +Adelie,Dream,37,16.9,185,3000,FEMALE +Adelie,Dream,39.6,18.8,190,4600,MALE +Adelie,Dream,41.1,19,182,3425,MALE +Adelie,Dream,37.5,18.9,179,2975, +Adelie,Dream,36,17.9,190,3450,FEMALE +Adelie,Dream,42.3,21.2,191,4150,MALE +Adelie,Biscoe,39.6,17.7,186,3500,FEMALE +Adelie,Biscoe,40.1,18.9,188,4300,MALE +Adelie,Biscoe,35,17.9,190,3450,FEMALE +Adelie,Biscoe,42,19.5,200,4050,MALE +Adelie,Biscoe,34.5,18.1,187,2900,FEMALE +Adelie,Biscoe,41.4,18.6,191,3700,MALE +Adelie,Biscoe,39,17.5,186,3550,FEMALE +Adelie,Biscoe,40.6,18.8,193,3800,MALE +Adelie,Biscoe,36.5,16.6,181,2850,FEMALE +Adelie,Biscoe,37.6,19.1,194,3750,MALE +Adelie,Biscoe,35.7,16.9,185,3150,FEMALE +Adelie,Biscoe,41.3,21.1,195,4400,MALE +Adelie,Biscoe,37.6,17,185,3600,FEMALE +Adelie,Biscoe,41.1,18.2,192,4050,MALE +Adelie,Biscoe,36.4,17.1,184,2850,FEMALE +Adelie,Biscoe,41.6,18,192,3950,MALE +Adelie,Biscoe,35.5,16.2,195,3350,FEMALE +Adelie,Biscoe,41.1,19.1,188,4100,MALE +Adelie,Torgersen,35.9,16.6,190,3050,FEMALE +Adelie,Torgersen,41.8,19.4,198,4450,MALE +Adelie,Torgersen,33.5,19,190,3600,FEMALE +Adelie,Torgersen,39.7,18.4,190,3900,MALE +Adelie,Torgersen,39.6,17.2,196,3550,FEMALE +Adelie,Torgersen,45.8,18.9,197,4150,MALE +Adelie,Torgersen,35.5,17.5,190,3700,FEMALE +Adelie,Torgersen,42.8,18.5,195,4250,MALE +Adelie,Torgersen,40.9,16.8,191,3700,FEMALE +Adelie,Torgersen,37.2,19.4,184,3900,MALE +Adelie,Torgersen,36.2,16.1,187,3550,FEMALE +Adelie,Torgersen,42.1,19.1,195,4000,MALE +Adelie,Torgersen,34.6,17.2,189,3200,FEMALE +Adelie,Torgersen,42.9,17.6,196,4700,MALE +Adelie,Torgersen,36.7,18.8,187,3800,FEMALE +Adelie,Torgersen,35.1,19.4,193,4200,MALE +Adelie,Dream,37.3,17.8,191,3350,FEMALE +Adelie,Dream,41.3,20.3,194,3550,MALE +Adelie,Dream,36.3,19.5,190,3800,MALE +Adelie,Dream,36.9,18.6,189,3500,FEMALE +Adelie,Dream,38.3,19.2,189,3950,MALE +Adelie,Dream,38.9,18.8,190,3600,FEMALE +Adelie,Dream,35.7,18,202,3550,FEMALE +Adelie,Dream,41.1,18.1,205,4300,MALE +Adelie,Dream,34,17.1,185,3400,FEMALE +Adelie,Dream,39.6,18.1,186,4450,MALE +Adelie,Dream,36.2,17.3,187,3300,FEMALE +Adelie,Dream,40.8,18.9,208,4300,MALE +Adelie,Dream,38.1,18.6,190,3700,FEMALE +Adelie,Dream,40.3,18.5,196,4350,MALE +Adelie,Dream,33.1,16.1,178,2900,FEMALE +Adelie,Dream,43.2,18.5,192,4100,MALE +Adelie,Biscoe,35,17.9,192,3725,FEMALE +Adelie,Biscoe,41,20,203,4725,MALE +Adelie,Biscoe,37.7,16,183,3075,FEMALE +Adelie,Biscoe,37.8,20,190,4250,MALE +Adelie,Biscoe,37.9,18.6,193,2925,FEMALE +Adelie,Biscoe,39.7,18.9,184,3550,MALE +Adelie,Biscoe,38.6,17.2,199,3750,FEMALE +Adelie,Biscoe,38.2,20,190,3900,MALE +Adelie,Biscoe,38.1,17,181,3175,FEMALE +Adelie,Biscoe,43.2,19,197,4775,MALE +Adelie,Biscoe,38.1,16.5,198,3825,FEMALE +Adelie,Biscoe,45.6,20.3,191,4600,MALE +Adelie,Biscoe,39.7,17.7,193,3200,FEMALE +Adelie,Biscoe,42.2,19.5,197,4275,MALE +Adelie,Biscoe,39.6,20.7,191,3900,FEMALE +Adelie,Biscoe,42.7,18.3,196,4075,MALE +Adelie,Torgersen,38.6,17,188,2900,FEMALE +Adelie,Torgersen,37.3,20.5,199,3775,MALE +Adelie,Torgersen,35.7,17,189,3350,FEMALE +Adelie,Torgersen,41.1,18.6,189,3325,MALE +Adelie,Torgersen,36.2,17.2,187,3150,FEMALE +Adelie,Torgersen,37.7,19.8,198,3500,MALE +Adelie,Torgersen,40.2,17,176,3450,FEMALE +Adelie,Torgersen,41.4,18.5,202,3875,MALE +Adelie,Torgersen,35.2,15.9,186,3050,FEMALE +Adelie,Torgersen,40.6,19,199,4000,MALE +Adelie,Torgersen,38.8,17.6,191,3275,FEMALE +Adelie,Torgersen,41.5,18.3,195,4300,MALE +Adelie,Torgersen,39,17.1,191,3050,FEMALE +Adelie,Torgersen,44.1,18,210,4000,MALE +Adelie,Torgersen,38.5,17.9,190,3325,FEMALE +Adelie,Torgersen,43.1,19.2,197,3500,MALE +Adelie,Dream,36.8,18.5,193,3500,FEMALE +Adelie,Dream,37.5,18.5,199,4475,MALE +Adelie,Dream,38.1,17.6,187,3425,FEMALE +Adelie,Dream,41.1,17.5,190,3900,MALE +Adelie,Dream,35.6,17.5,191,3175,FEMALE +Adelie,Dream,40.2,20.1,200,3975,MALE +Adelie,Dream,37,16.5,185,3400,FEMALE +Adelie,Dream,39.7,17.9,193,4250,MALE +Adelie,Dream,40.2,17.1,193,3400,FEMALE +Adelie,Dream,40.6,17.2,187,3475,MALE +Adelie,Dream,32.1,15.5,188,3050,FEMALE +Adelie,Dream,40.7,17,190,3725,MALE +Adelie,Dream,37.3,16.8,192,3000,FEMALE +Adelie,Dream,39,18.7,185,3650,MALE +Adelie,Dream,39.2,18.6,190,4250,MALE +Adelie,Dream,36.6,18.4,184,3475,FEMALE +Adelie,Dream,36,17.8,195,3450,FEMALE +Adelie,Dream,37.8,18.1,193,3750,MALE +Adelie,Dream,36,17.1,187,3700,FEMALE +Adelie,Dream,41.5,18.5,201,4000,MALE +Chinstrap,Dream,46.5,17.9,192,3500,FEMALE +Chinstrap,Dream,50,19.5,196,3900,MALE +Chinstrap,Dream,51.3,19.2,193,3650,MALE +Chinstrap,Dream,45.4,18.7,188,3525,FEMALE +Chinstrap,Dream,52.7,19.8,197,3725,MALE +Chinstrap,Dream,45.2,17.8,198,3950,FEMALE +Chinstrap,Dream,46.1,18.2,178,3250,FEMALE +Chinstrap,Dream,51.3,18.2,197,3750,MALE +Chinstrap,Dream,46,18.9,195,4150,FEMALE +Chinstrap,Dream,51.3,19.9,198,3700,MALE +Chinstrap,Dream,46.6,17.8,193,3800,FEMALE +Chinstrap,Dream,51.7,20.3,194,3775,MALE +Chinstrap,Dream,47,17.3,185,3700,FEMALE +Chinstrap,Dream,52,18.1,201,4050,MALE +Chinstrap,Dream,45.9,17.1,190,3575,FEMALE +Chinstrap,Dream,50.5,19.6,201,4050,MALE +Chinstrap,Dream,50.3,20,197,3300,MALE +Chinstrap,Dream,58,17.8,181,3700,FEMALE +Chinstrap,Dream,46.4,18.6,190,3450,FEMALE +Chinstrap,Dream,49.2,18.2,195,4400,MALE +Chinstrap,Dream,42.4,17.3,181,3600,FEMALE +Chinstrap,Dream,48.5,17.5,191,3400,MALE +Chinstrap,Dream,43.2,16.6,187,2900,FEMALE +Chinstrap,Dream,50.6,19.4,193,3800,MALE +Chinstrap,Dream,46.7,17.9,195,3300,FEMALE +Chinstrap,Dream,52,19,197,4150,MALE +Chinstrap,Dream,50.5,18.4,200,3400,FEMALE +Chinstrap,Dream,49.5,19,200,3800,MALE +Chinstrap,Dream,46.4,17.8,191,3700,FEMALE +Chinstrap,Dream,52.8,20,205,4550,MALE +Chinstrap,Dream,40.9,16.6,187,3200,FEMALE +Chinstrap,Dream,54.2,20.8,201,4300,MALE +Chinstrap,Dream,42.5,16.7,187,3350,FEMALE +Chinstrap,Dream,51,18.8,203,4100,MALE +Chinstrap,Dream,49.7,18.6,195,3600,MALE +Chinstrap,Dream,47.5,16.8,199,3900,FEMALE +Chinstrap,Dream,47.6,18.3,195,3850,FEMALE +Chinstrap,Dream,52,20.7,210,4800,MALE +Chinstrap,Dream,46.9,16.6,192,2700,FEMALE +Chinstrap,Dream,53.5,19.9,205,4500,MALE +Chinstrap,Dream,49,19.5,210,3950,MALE +Chinstrap,Dream,46.2,17.5,187,3650,FEMALE +Chinstrap,Dream,50.9,19.1,196,3550,MALE +Chinstrap,Dream,45.5,17,196,3500,FEMALE +Chinstrap,Dream,50.9,17.9,196,3675,FEMALE +Chinstrap,Dream,50.8,18.5,201,4450,MALE +Chinstrap,Dream,50.1,17.9,190,3400,FEMALE +Chinstrap,Dream,49,19.6,212,4300,MALE +Chinstrap,Dream,51.5,18.7,187,3250,MALE +Chinstrap,Dream,49.8,17.3,198,3675,FEMALE +Chinstrap,Dream,48.1,16.4,199,3325,FEMALE +Chinstrap,Dream,51.4,19,201,3950,MALE +Chinstrap,Dream,45.7,17.3,193,3600,FEMALE +Chinstrap,Dream,50.7,19.7,203,4050,MALE +Chinstrap,Dream,42.5,17.3,187,3350,FEMALE +Chinstrap,Dream,52.2,18.8,197,3450,MALE +Chinstrap,Dream,45.2,16.6,191,3250,FEMALE +Chinstrap,Dream,49.3,19.9,203,4050,MALE +Chinstrap,Dream,50.2,18.8,202,3800,MALE +Chinstrap,Dream,45.6,19.4,194,3525,FEMALE +Chinstrap,Dream,51.9,19.5,206,3950,MALE +Chinstrap,Dream,46.8,16.5,189,3650,FEMALE +Chinstrap,Dream,45.7,17,195,3650,FEMALE +Chinstrap,Dream,55.8,19.8,207,4000,MALE +Chinstrap,Dream,43.5,18.1,202,3400,FEMALE +Chinstrap,Dream,49.6,18.2,193,3775,MALE +Chinstrap,Dream,50.8,19,210,4100,MALE +Chinstrap,Dream,50.2,18.7,198,3775,FEMALE +Gentoo,Biscoe,46.1,13.2,211,4500,FEMALE +Gentoo,Biscoe,50,16.3,230,5700,MALE +Gentoo,Biscoe,48.7,14.1,210,4450,FEMALE +Gentoo,Biscoe,50,15.2,218,5700,MALE +Gentoo,Biscoe,47.6,14.5,215,5400,MALE +Gentoo,Biscoe,46.5,13.5,210,4550,FEMALE +Gentoo,Biscoe,45.4,14.6,211,4800,FEMALE +Gentoo,Biscoe,46.7,15.3,219,5200,MALE +Gentoo,Biscoe,43.3,13.4,209,4400,FEMALE +Gentoo,Biscoe,46.8,15.4,215,5150,MALE +Gentoo,Biscoe,40.9,13.7,214,4650,FEMALE +Gentoo,Biscoe,49,16.1,216,5550,MALE +Gentoo,Biscoe,45.5,13.7,214,4650,FEMALE +Gentoo,Biscoe,48.4,14.6,213,5850,MALE +Gentoo,Biscoe,45.8,14.6,210,4200,FEMALE +Gentoo,Biscoe,49.3,15.7,217,5850,MALE +Gentoo,Biscoe,42,13.5,210,4150,FEMALE +Gentoo,Biscoe,49.2,15.2,221,6300,MALE +Gentoo,Biscoe,46.2,14.5,209,4800,FEMALE +Gentoo,Biscoe,48.7,15.1,222,5350,MALE +Gentoo,Biscoe,50.2,14.3,218,5700,MALE +Gentoo,Biscoe,45.1,14.5,215,5000,FEMALE +Gentoo,Biscoe,46.5,14.5,213,4400,FEMALE +Gentoo,Biscoe,46.3,15.8,215,5050,MALE +Gentoo,Biscoe,42.9,13.1,215,5000,FEMALE +Gentoo,Biscoe,46.1,15.1,215,5100,MALE +Gentoo,Biscoe,44.5,14.3,216,4100, +Gentoo,Biscoe,47.8,15,215,5650,MALE +Gentoo,Biscoe,48.2,14.3,210,4600,FEMALE +Gentoo,Biscoe,50,15.3,220,5550,MALE +Gentoo,Biscoe,47.3,15.3,222,5250,MALE +Gentoo,Biscoe,42.8,14.2,209,4700,FEMALE +Gentoo,Biscoe,45.1,14.5,207,5050,FEMALE +Gentoo,Biscoe,59.6,17,230,6050,MALE +Gentoo,Biscoe,49.1,14.8,220,5150,FEMALE +Gentoo,Biscoe,48.4,16.3,220,5400,MALE +Gentoo,Biscoe,42.6,13.7,213,4950,FEMALE +Gentoo,Biscoe,44.4,17.3,219,5250,MALE +Gentoo,Biscoe,44,13.6,208,4350,FEMALE +Gentoo,Biscoe,48.7,15.7,208,5350,MALE +Gentoo,Biscoe,42.7,13.7,208,3950,FEMALE +Gentoo,Biscoe,49.6,16,225,5700,MALE +Gentoo,Biscoe,45.3,13.7,210,4300,FEMALE +Gentoo,Biscoe,49.6,15,216,4750,MALE +Gentoo,Biscoe,50.5,15.9,222,5550,MALE +Gentoo,Biscoe,43.6,13.9,217,4900,FEMALE +Gentoo,Biscoe,45.5,13.9,210,4200,FEMALE +Gentoo,Biscoe,50.5,15.9,225,5400,MALE +Gentoo,Biscoe,44.9,13.3,213,5100,FEMALE +Gentoo,Biscoe,45.2,15.8,215,5300,MALE +Gentoo,Biscoe,46.6,14.2,210,4850,FEMALE +Gentoo,Biscoe,48.5,14.1,220,5300,MALE +Gentoo,Biscoe,45.1,14.4,210,4400,FEMALE +Gentoo,Biscoe,50.1,15,225,5000,MALE +Gentoo,Biscoe,46.5,14.4,217,4900,FEMALE +Gentoo,Biscoe,45,15.4,220,5050,MALE +Gentoo,Biscoe,43.8,13.9,208,4300,FEMALE +Gentoo,Biscoe,45.5,15,220,5000,MALE +Gentoo,Biscoe,43.2,14.5,208,4450,FEMALE +Gentoo,Biscoe,50.4,15.3,224,5550,MALE +Gentoo,Biscoe,45.3,13.8,208,4200,FEMALE +Gentoo,Biscoe,46.2,14.9,221,5300,MALE +Gentoo,Biscoe,45.7,13.9,214,4400,FEMALE +Gentoo,Biscoe,54.3,15.7,231,5650,MALE +Gentoo,Biscoe,45.8,14.2,219,4700,FEMALE +Gentoo,Biscoe,49.8,16.8,230,5700,MALE +Gentoo,Biscoe,46.2,14.4,214,4650, +Gentoo,Biscoe,49.5,16.2,229,5800,MALE +Gentoo,Biscoe,43.5,14.2,220,4700,FEMALE +Gentoo,Biscoe,50.7,15,223,5550,MALE +Gentoo,Biscoe,47.7,15,216,4750,FEMALE +Gentoo,Biscoe,46.4,15.6,221,5000,MALE +Gentoo,Biscoe,48.2,15.6,221,5100,MALE +Gentoo,Biscoe,46.5,14.8,217,5200,FEMALE +Gentoo,Biscoe,46.4,15,216,4700,FEMALE +Gentoo,Biscoe,48.6,16,230,5800,MALE +Gentoo,Biscoe,47.5,14.2,209,4600,FEMALE +Gentoo,Biscoe,51.1,16.3,220,6000,MALE +Gentoo,Biscoe,45.2,13.8,215,4750,FEMALE +Gentoo,Biscoe,45.2,16.4,223,5950,MALE +Gentoo,Biscoe,49.1,14.5,212,4625,FEMALE +Gentoo,Biscoe,52.5,15.6,221,5450,MALE +Gentoo,Biscoe,47.4,14.6,212,4725,FEMALE +Gentoo,Biscoe,50,15.9,224,5350,MALE +Gentoo,Biscoe,44.9,13.8,212,4750,FEMALE +Gentoo,Biscoe,50.8,17.3,228,5600,MALE +Gentoo,Biscoe,43.4,14.4,218,4600,FEMALE +Gentoo,Biscoe,51.3,14.2,218,5300,MALE +Gentoo,Biscoe,47.5,14,212,4875,FEMALE +Gentoo,Biscoe,52.1,17,230,5550,MALE +Gentoo,Biscoe,47.5,15,218,4950,FEMALE +Gentoo,Biscoe,52.2,17.1,228,5400,MALE +Gentoo,Biscoe,45.5,14.5,212,4750,FEMALE +Gentoo,Biscoe,49.5,16.1,224,5650,MALE +Gentoo,Biscoe,44.5,14.7,214,4850,FEMALE +Gentoo,Biscoe,50.8,15.7,226,5200,MALE +Gentoo,Biscoe,49.4,15.8,216,4925,MALE +Gentoo,Biscoe,46.9,14.6,222,4875,FEMALE +Gentoo,Biscoe,48.4,14.4,203,4625,FEMALE +Gentoo,Biscoe,51.1,16.5,225,5250,MALE +Gentoo,Biscoe,48.5,15,219,4850,FEMALE +Gentoo,Biscoe,55.9,17,228,5600,MALE +Gentoo,Biscoe,47.2,15.5,215,4975,FEMALE +Gentoo,Biscoe,49.1,15,228,5500,MALE +Gentoo,Biscoe,47.3,13.8,216,4725, +Gentoo,Biscoe,46.8,16.1,215,5500,MALE +Gentoo,Biscoe,41.7,14.7,210,4700,FEMALE +Gentoo,Biscoe,53.4,15.8,219,5500,MALE +Gentoo,Biscoe,43.3,14,208,4575,FEMALE +Gentoo,Biscoe,48.1,15.1,209,5500,MALE +Gentoo,Biscoe,50.5,15.2,216,5000,FEMALE +Gentoo,Biscoe,49.8,15.9,229,5950,MALE +Gentoo,Biscoe,43.5,15.2,213,4650,FEMALE +Gentoo,Biscoe,51.5,16.3,230,5500,MALE +Gentoo,Biscoe,46.2,14.1,217,4375,FEMALE +Gentoo,Biscoe,55.1,16,230,5850,MALE +Gentoo,Biscoe,44.5,15.7,217,4875, +Gentoo,Biscoe,48.8,16.2,222,6000,MALE +Gentoo,Biscoe,47.2,13.7,214,4925,FEMALE +Gentoo,Biscoe,NaN,NaN,NaN,NaN, +Gentoo,Biscoe,46.8,14.3,215,4850,FEMALE +Gentoo,Biscoe,50.4,15.7,222,5750,MALE +Gentoo,Biscoe,45.2,14.8,212,5200,FEMALE +Gentoo,Biscoe,49.9,16.1,213,5400,MALE \ No newline at end of file diff --git a/src/content/tutorial/02-advanced/01-tooltips/01-intro/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/02-advanced/01-tooltips/01-intro/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..86457d625 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/01-intro/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,16 @@ + + + + + + diff --git a/src/content/tutorial/02-advanced/01-tooltips/01-intro/index.md b/src/content/tutorial/02-advanced/01-tooltips/01-intro/index.md new file mode 100644 index 000000000..09b685fe7 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/01-intro/index.md @@ -0,0 +1,7 @@ +--- +title: Tooltips +--- + +Interactive charts often benefit from tooltips — small overlays that reveal additional details when the user points to an element. + +Over the next few steps we'll add progressively richer tooltip interactions to a simple bar chart, then move on to scatter plots. diff --git a/src/content/tutorial/02-advanced/01-tooltips/02-pointer-events/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/02-advanced/01-tooltips/02-pointer-events/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..84c4429f2 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/02-pointer-events/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,28 @@ + + + + (tooltip = d)} + onpointerleave={() => (tooltip = null)} + /> + + + +{#if tooltip} +

    {tooltip.fruit}: {tooltip.sales} units

    +{/if} diff --git a/src/content/tutorial/02-advanced/01-tooltips/02-pointer-events/index.md b/src/content/tutorial/02-advanced/01-tooltips/02-pointer-events/index.md new file mode 100644 index 000000000..c942a7570 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/02-pointer-events/index.md @@ -0,0 +1,33 @@ +--- +title: Pointer events +--- + +Every mark in SveltePlot supports native pointer events. The `onpointerenter` handler receives the browser event and the **datum** — the original data record for the element being pointed at. + +Add a state variable and wire it up to the bar mark: + +```svelte + const data = [...]; + ++ let tooltip = $state(null); +``` + +```svelte + (tooltip = d)} ++ onpointerleave={() => (tooltip = null)} + /> +``` + +Finally, display the tooltip below the chart: + +```svelte +
    + ++{#if tooltip} ++

    {tooltip.fruit}: {tooltip.sales} units

    ++{/if} +``` diff --git a/src/content/tutorial/02-advanced/01-tooltips/03-text-labels/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/02-advanced/01-tooltips/03-text-labels/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..727782153 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/03-text-labels/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,33 @@ + + + + (d === tooltip ? 1 : 0.3) : 1} + onpointerenter={(e, d) => (tooltip = d)} + onpointerleave={() => (tooltip = null)} + /> + + String(d.sales)} + lineAnchor="bottom" + dy={-4} + /> + diff --git a/src/content/tutorial/02-advanced/01-tooltips/03-text-labels/index.md b/src/content/tutorial/02-advanced/01-tooltips/03-text-labels/index.md new file mode 100644 index 000000000..b973d24c6 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/03-text-labels/index.md @@ -0,0 +1,44 @@ +--- +title: Text labels +--- + +Showing the hovered value below the chart works, but it would be better to show it directly inside the chart. We can replace the `

    ` tag with a `Text` mark. + +First, import `Text` and remove the paragraph below the chart: + +```svelte +- import { Plot, BarY, RuleY } from 'svelteplot'; ++ import { Plot, BarY, RuleY, Text } from 'svelteplot'; +``` + +Then add the `Text` mark inside ``. Pass only the hovered datum as a single-element array — when nothing is hovered, pass an empty array: + +```svelte + ++ String(d.sales)} ++ lineAnchor="bottom" ++ dy={-4} ++ /> + + +-{#if tooltip} +-

    {tooltip.fruit}: {tooltip.sales} units

    +-{/if} +``` + +While we're at it, dim the unselected bars to draw attention to the hovered one: + +```svelte + (d === tooltip ? 1 : 0.3) : 1} + onpointerenter={(e, d) => (tooltip = d)} + onpointerleave={() => (tooltip = null)} + /> +``` diff --git a/src/content/tutorial/02-advanced/01-tooltips/04-voronoi/+assets/app-a/src/lib/App.svelte b/src/content/tutorial/02-advanced/01-tooltips/04-voronoi/+assets/app-a/src/lib/App.svelte new file mode 100644 index 000000000..1b5ed936a --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/04-voronoi/+assets/app-a/src/lib/App.svelte @@ -0,0 +1,28 @@ + + + + (tooltip = d)} + onpointerleave={() => (tooltip = null)} + stroke={(d) => + isEqual(tooltip, d) + ? 'currentColor' + : 'transparent'} /> + + +{#if tooltip} +

    + {tooltip.species}: {tooltip.bill_length_mm} mm × {tooltip.bill_depth_mm} + mm +

    +{/if} diff --git a/src/content/tutorial/02-advanced/01-tooltips/04-voronoi/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/02-advanced/01-tooltips/04-voronoi/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..ce068e098 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/04-voronoi/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,35 @@ + + + + + isEqual(tooltip, d) + ? 'currentColor' + : 'transparent'} /> + (tooltip = d)} + onpointerleave={() => (tooltip = null)} /> + + +{#if tooltip} +

    + {tooltip.species}: {tooltip.bill_length_mm} mm × {tooltip.bill_depth_mm} + mm +

    +{/if} diff --git a/src/content/tutorial/02-advanced/01-tooltips/04-voronoi/index.md b/src/content/tutorial/02-advanced/01-tooltips/04-voronoi/index.md new file mode 100644 index 000000000..3fee33475 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/04-voronoi/index.md @@ -0,0 +1,36 @@ +--- +title: Voronoi tooltips +--- + +The pointer-events approach works great for bars, but in a scatter plot the `Dot` symbols are often too small to hover over reliably. One solution is to overlay an invisible **Voronoi tessellation** — a set of polygons that partition the plot area so that every pixel belongs to the nearest data point. + +Import the `Voronoi` mark and add it on top of the dots: + +```js +import { Plot, Dot+++, Voronoi+++ } from 'svelteplot'; +``` + +Place the `Voronoi` mark after `` so it sits on top and receives the pointer events. Use `fill="transparent"` so the polygons are invisible but still catchable: + +```svelte + ++ (tooltip = d)} ++ onpointerleave={() => (tooltip = null)} ++ /> + +``` + +```svelte +
    + ++{#if tooltip} ++

    {tooltip.species}: {tooltip.bill_length_mm} mm × {tooltip.bill_depth_mm} mm

    ++{/if} +``` diff --git a/src/content/tutorial/02-advanced/01-tooltips/05-pointer-mark/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/02-advanced/01-tooltips/05-pointer-mark/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..bdf887b0c --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/05-pointer-mark/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,33 @@ + + + + + isEqual(tooltip, d) + ? 'currentColor' + : 'transparent'} /> + + (tooltip = selection[0] ?? null)} /> + + + +{#if tooltip} +

    + {tooltip.species}: {tooltip.bill_length_mm} mm × {tooltip.bill_depth_mm} + mm +

    +{/if} diff --git a/src/content/tutorial/02-advanced/01-tooltips/05-pointer-mark/index.md b/src/content/tutorial/02-advanced/01-tooltips/05-pointer-mark/index.md new file mode 100644 index 000000000..62ffaba69 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/05-pointer-mark/index.md @@ -0,0 +1,31 @@ +--- +title: Pointer mark +--- + +The `Voronoi` approach requires computing and rendering extra geometry. SveltePlot has a simpler alternative: the `Pointer` mark, which uses a **proximity quadtree** to find the nearest data point to the cursor without adding any visible elements. + +Replace `Voronoi` with `Pointer`. Instead of `onpointerenter`/`onpointerleave`, use the `onupdate` callback — it fires whenever the selection changes and receives the array of currently selected data points: + +```js +import { Plot, Dot, ---Voronoi---+++Pointer+++ } from 'svelteplot'; +``` + +```svelte +- (tooltip = d)} +- onpointerleave={() => (tooltip = null)} +- /> ++ (tooltip = selection[0] ?? null)} ++ /> +``` + +The `maxDistance` prop (default `15px`) controls how close the cursor must be to select a point. When the cursor moves away, `onupdate` fires with an empty array, so `selection[0] ?? null` clears the tooltip. diff --git a/src/content/tutorial/02-advanced/01-tooltips/06-html-tooltip/+assets/app-b/src/lib/App.svelte b/src/content/tutorial/02-advanced/01-tooltips/06-html-tooltip/+assets/app-b/src/lib/App.svelte new file mode 100644 index 000000000..1ee8f2033 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/06-html-tooltip/+assets/app-b/src/lib/App.svelte @@ -0,0 +1,42 @@ + + + + + {#snippet overlay()} + + {#snippet children({ datum })} +
    + {datum?.species}
    + bill: {datum?.bill_length_mm} × {datum?.bill_depth_mm} + mm +
    + {/snippet} +
    + {/snippet} +
    + + diff --git a/src/content/tutorial/02-advanced/01-tooltips/06-html-tooltip/index.md b/src/content/tutorial/02-advanced/01-tooltips/06-html-tooltip/index.md new file mode 100644 index 000000000..7e455d1ed --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/06-html-tooltip/index.md @@ -0,0 +1,31 @@ +--- +title: HTML tooltip +--- + +Managing tooltip state and positioning manually works, but SveltePlot ships with a `HTMLTooltip` mark that handles both automatically. It uses the same quadtree proximity logic as `Pointer` but renders a floating HTML `
    ` positioned at the hovered data point's screen coordinates. + +Remove the `Pointer` mark, the `tooltip` state, and the `{#if}` block. Import `HTMLTooltip` instead: + +```svelte +- import { Plot, Dot, Frame, Pointer } from 'svelteplot'; ++ import { Plot, Dot, Frame, HTMLTooltip } from 'svelteplot'; +``` + +Replace the `` mark and the `{#if}` block with ``. The `children` snippet receives `{ datum }` — the nearest data point: + +```svelte +- (tooltip = selection[0] ?? null)} +- /> ++ ++ {#snippet children({ datum })} ++ {datum?.species}
    ++ bill: {datum?.bill_length_mm} × {datum?.bill_depth_mm} mm ++ {/snippet} ++
    +``` + +The tooltip `
    ` is positioned absolutely inside the plot and hidden when no point is nearby. Style it with CSS to match your design. diff --git a/src/content/tutorial/02-advanced/01-tooltips/index.md b/src/content/tutorial/02-advanced/01-tooltips/index.md new file mode 100644 index 000000000..aee55df84 --- /dev/null +++ b/src/content/tutorial/02-advanced/01-tooltips/index.md @@ -0,0 +1,3 @@ +--- +title: Tooltips +--- diff --git a/src/content/tutorial/02-advanced/index.md b/src/content/tutorial/02-advanced/index.md new file mode 100644 index 000000000..5029da7ef --- /dev/null +++ b/src/content/tutorial/02-advanced/index.md @@ -0,0 +1,6 @@ +--- +title: SveltePlot +label: Advanced +scope: { 'prefix': '/src/lib/', 'name': 'src' } +focus: /src/lib/App.svelte +--- diff --git a/src/content/tutorial/PLAN.md b/src/content/tutorial/PLAN.md new file mode 100644 index 000000000..7d8912f2e --- /dev/null +++ b/src/content/tutorial/PLAN.md @@ -0,0 +1,313 @@ +# Tutorial Plan + +## Goal + +Give new users a **playful, interactive introduction** to the mental model behind SveltePlot — not a complete feature reference. The tutorial is done when a user understands _why_ SveltePlot works the way it does and feels confident enough to read the docs on their own. + +**The tutorial's job:** concepts. **The docs' job:** every option on every mark. + +## Scope — what "done" looks like + +The basics tutorial is **already essentially complete**: + +- **01-basics** — getting-started + scales + axes/grids + faceting (≈15 lessons ✓) + +The marks and transforms chapters inside 01-basics are **deferred**. Instead of duplicating the docs, mark-specific tutorials (e.g. "working with the Line mark") should be linked directly from the relevant docs pages. This keeps the main tutorial focused and finite. + +**02-advanced** is a placeholder for future advanced tutorials. + +--- + +Three levels map to the three directory levels: Part → Chapter → Lesson (REPL step). + +--- + +## Tutorial structure + +### Directory layout + +``` +/ e.g. 01-basics/ + index.md part title + optional intro text + +assets/ shared files for every lesson in the part + src/lib/penguins.csv + src/routes/+page.svelte + ... + / e.g. 01-scales/ + index.md chapter title (body usually empty) + +assets/ shared files for every lesson in this chapter + src/lib/aapl.csv + / e.g. 01-zero-rule/ + index.md lesson prose + code diffs (frontmatter: title) + +assets/ + app-a/src/lib/App.svelte starting state shown to the user + app-b/src/lib/App.svelte solution state (shown when "Solve" is clicked) +``` + +### File merging + +The REPL's starting state (`a`) is built by merging three layers in order: + +1. **Part-level** `+assets/` — routes boilerplate, shared datasets +2. **Chapter-level** `+assets/` — data files used across all lessons in the chapter +3. **Lesson `app-a/`** — the editable `App.svelte` and any lesson-specific files + +When the user clicks **Solve**, only the `app-b/` files are merged on top of `a`. This means `app-b` only needs to contain files that actually change — typically just `App.svelte`. All other files (CSV data, routes, etc.) carry over automatically. + +### Lesson prose conventions + +- Full-line add/remove: prefix the line with `+` or `-` (single character). +- Inline add/remove: wrap spans with `+++text+++` and `---text---`; multiple pairs on the same line are fine. +- Show the import change in a separate code block when a new component is imported. +- Keep prose short: one sentence of context, one instruction, one explanation of why. + +--- + +## 01 · Basics (`01-basics/`) + +### Getting started (`01-getting-started/`) + +The idea of the basics section is to learn how to construct a plot with SveltePlot by combining the Plot component with marks and transforms. + +- [x] Your first plot → `01-getting-started/01-first-plot` +- [x] Marks — what they are, swapping one for another → `01-getting-started/02-marks` +- [x] Layering marks → `01-getting-started/03-layering` +- [x] Channels (x, y, fill) → `01-getting-started/04-channels` +- [x] Color channels — quantitative fill → `01-getting-started/05-color-channels` +- [x] Reactivity → `01-getting-started/08-reactivity` + +### Scales (`02-scales/`) + +In this chapter we learn how SveltePlot automatically determines the scales for our plot and how to customize them. + +- Automatic scale domains +- Setting scale options: log, nice, padding, domain, etc. +- Bypassing scales + - change `` to `` to bypass scale + +### Transforms (`03-transforms/`) — DEFERRED + +Transforms are useful when your dataset is not in the shape you need it to be visualized. Technically, the BarY mark requires + +> See `## Transforms — DEFERRED` below. + +### Scales (`04-scales/`) + +The Plot component collects data from all marks and determines common scales. + +- [x] Extending the domain — add `` → `04-scales/01-zero-rule` +- [x] The zero option — `y={{ zero: true }}` → `04-scales/02-zero-option` +- [x] Scale types — add `` → `04-scales/03-scale-type` + +### Axes & grids (`05-axes-grids/`) + +Axes are auto-added; they can be disabled or overridden. + +- [x] Implicit axes → `05-axes-grids/01-implicit-marks` +- [x] Explicit axes → `05-axes-grids/02-explicit-axes` +- [x] Implicit grids → `05-axes-grids/03-implicit-grids` +- [x] Explicit grids → `05-axes-grids/04-explicit-grids` + +### Faceting (`06-faceting/`) + +Plots can be split into multiple panels via `fx`/`fy` channels. + +- [x] Faceting basics → `06-faceting/01-faceting` +- [x] Unfaceted data in every panel → `06-faceting/02-unfaceted-data` + +--- + +## 02 · Advanced (`02-advanced/`) — PLACEHOLDER + +No lessons yet. Future candidates: plot defaults, overlays/underlays, HTML tooltips, image underlays. + +## Marks — DEFERRED (`01-basics/02-marks/`) + +> Not part of the main basics tutorial. Mark-specific tutorials (if written) should be short, self-contained, and linked from the corresponding docs page rather than chained into this tutorial. Answer to the question below: no. +> +> _Original question:_ do we really want to introduce all marks and their features? Isn't that duplicating the marks docs? Perhaps it should just touch on the different kinds of marks? + +- SVG vs HTML +- Canvas rendering +- Point vs range +- One-row-per-element (dot, bar) vs. multiple-rows-per-element (line, area, ...) +- Geo marks +- + +- **Marks** - Short introduction into the marks concept. Data, Channels, Style properties, common properties +- **Dot** (`01-basics/02-marks/01-dot/`) + - The dot mark is a very versatile mark. It can be used for scatterplots, dot plots, beeswarm plots (in combination with the [dodge transform]). + - [x] Symbol channel — shapes for categories → `01-dot/01-symbol` + - [x] DotX / DotY — one-dimensional strip → `01-dot/02-dotx-doty` + - [x] Size channel — bubble chart (r channel) → `01-dot/03-size-channel` + - [ ] Color & opacity + +- **Line** (`01-basics/02-marks/03-line/`) — Apple stock (aapl.csv) + - The line mark is useful for showing change. It accepts an array of positions that get connected through a line. + - [x] The Line mark — time series → `03-line/01-line` + - [x] Sorting — sort transform for non-temporal x → `03-line/02-sorting` - the line mark connects the data points in the order they appear in thet data. If your data comes in the wrong order, you can sort it before passing it to the line mark + - [ ] More convenient sorting using the built-in `sort` transform. + - [x] Multiple series — stroke channel + group → `03-line/03-grouping` + - [x] Markers — arrowhead / dot markers → `03-line/04-markers` + - [ ] Custom marker snippet + - [x] Curve — interpolation options → `03-line/05-curve` + - [x] Text along lines — inline labels → `03-line/06-text` + +- **Area** (TBD) + - [ ] AreaY — area below a line + - [ ] AreaX — horizontal area + - [ ] Band / range area (y1 and y2 channels) + - [ ] DifferenceY — difference / above-below chart + +- **Rect** (TBD) + - The rect mark is one of three marks that are rendering rectangles to a plot, depending on your x and y axis. Here's an overview on which marks to use in which scenario: + | | y scale is quantitative | y scale is qualitative | + | ------------------------------------ | :---------------------: | :--------------------: | + | **x scale is quantitative** | Rect | BarX | + | **x scale is qualitative** | BarY | Cell | + + - [ ] Rect: no stacking - The `Rect` mark requires four channes x1, x2, y1, y2 that define the bounds of each rectangle. _Create a plot with custom rectangles_ + - [ ] RectX and RectY for range annotations + - [ ] RectY + bin transform — basic histogram + - [ ] RectX — horizontal histogram + +- **Bar** (TBD) + - [ ] BarY — vertical bar chart + - [ ] BarX — horizontal bar chart + - [ ] Color channel on bars + +- **Cell** (TBD) + - [ ] Cell — basic heatmap (x/y as categories, fill as value) + - [ ] CellX / CellY — one axis is categorical + +- **Rule** (TBD) + - [ ] RuleY — horizontal reference line + - [ ] RuleX — vertical reference line + +- **Frame** + - [ ] Frames can be added implicitely by setting the frame property on the Plot component. But you can also add frames explicitely using the frame mark. + - [ ] Frames can be manipulated + - [ ] You can use a frame mark for clipping +- **Tick** (`01-basics/02-marks/02-tick/`) — faceted by species (`fy="species"`) + - [x] TickX / TickY — tick strip / rug plot → `02-tick/01-tickx-ticky` + +- **Text** (TBD) + - [ ] Text mark — positioning, formatting, alignment + +- **Arrow / Link** (TBD) + - [ ] Arrow — curved labeled arrows between points + - [ ] Link — straight line between two data points + +- **Vector** (TBD) + - [ ] Vector — directional / wind field + - [ ] Spike — magnitude as spike height + +- **Box** (TBD) + - [ ] BoxY — vertical box plot + - [ ] BoxX — horizontal box plot + +- **Regression** (TBD) + - [ ] RegressionY — linear regression line + - [ ] RegressionX + +- **Other statistical marks** (TBD) + - [ ] BollingerY + - [ ] Trail — temporal path with varying stroke width + +- **Geographic** (TBD) + - [ ] Geo — rendering GeoJSON features with a projection + - [ ] Sphere and Graticule — globe outline and grid lines + +- **Interaction** (TBD) + - [ ] Pointer mark — nearest-point hover / tooltip + - [ ] Tooltip mark + - [ ] BrushX / BrushY / Brush — selection + +- **Advanced** (TBD) + - [ ] Density / Contour + - [ ] DelaunayLink / Hull / Voronoi + - [ ] Raster + - [ ] CustomMark / CustomMarkHTML + +--- + +## Transforms — DEFERRED (`01-basics/03-transforms/`) + +> Not part of the main basics tutorial. Same rationale as 03-marks above. + +Transforms transform data into a shape that + +- **Stacking** + - [ ] Stacking means taking a single value and converting them into value ranges. Some marks implicitely stack your data, like AreaX or BarX, but you can also explicitely stack. + - [ ] Explicit stacking example + - [ ] Normalized stacks (offset: normalize) + +- **Jitter** (`01-basics/03-transforms/01-jitter/`) — Cars dataset + - [x] Jitter transform — spreading overlapping points → `01-jitter/01-jitter` + - [x] Reactive jitter — width control via $state → `01-jitter/02-reactive-jitter` + +- **Window** (TBD) + - [ ] Moving average — window transform + - [ ] Bollinger bands + +- **Bin** (TBD) + - [ ] Basic histogram (RectY + bin) + - [ ] Bin options — thresholds, step, domain + - [ ] 2D binning — frequency heatmap + +- **Group** (TBD) + - [ ] Grouped bar chart + - [ ] Aggregating into cells (Cell + group) + +- **Dodge** (TBD) + - [ ] Dodge — non-overlapping layout + - [ ] Beeswarm plot + +- **Normalize** (TBD) + +- **Density** (TBD) + - [ ] Density mark — kernel density estimation + - [ ] Contour mark — density contour lines + +## Mark families + +Single x or y position: + +- Rule +- Tick +- Axis +- Grid + +x/y Position only: + +- Dot +- Vector (x/y + rotation + length) +- Image +- Text +- Custom + +Start and end positions + +- Link +- Arrow +- Rect + +Geographical marks + +- Geo +- Sphere +- Graticule + +Multiple data points to one element: + +- Line +- Area +- Trail +- Density +- Contour + +Composite marks: + +- Difference +- Box +- Regression diff --git a/src/lib/server/shikiDiffNotation.ts b/src/lib/server/shikiDiffNotation.ts new file mode 100644 index 000000000..53be9a83c --- /dev/null +++ b/src/lib/server/shikiDiffNotation.ts @@ -0,0 +1,143 @@ +import type { DecorationItem, ShikiTransformer } from 'shiki'; + +export function shikiFileHeader(className = 'line--file-header'): ShikiTransformer { + return { + name: 'shiki-file-header', + line(node) { + const text = node.children + .flatMap((child) => (child.type === 'element' ? child.children : [])) + .filter((n) => n.type === 'text') + .map((n) => n.value) + .join(''); + if (text.startsWith('/// file:')) { + this.addClassToHast(node, className); + } + } + }; +} + +interface ShikiDiffNotationOptions { + classLineAdd?: string; + classLineRemove?: string; + classInlineAdd?: string; + classInlineRemove?: string; + classActivePre?: string; +} + +/** Strip inline +++text+++ or ---text--- markers from a line, returning the clean + * line text and any decoration ranges (positions are offsets into the clean text). */ +function processInlineMarkers( + line: string, + lineIndex: number, + marker: '+++' | '---', + className: string, + decorations: DecorationItem[] +): string { + if (!line.includes(marker)) return line; + const parts = line.split(marker); + let cleanLine = ''; + let charPos = 0; + for (let i = 0; i < parts.length; i++) { + cleanLine += parts[i]; + if (i % 2 === 1 && parts[i].length > 0) { + decorations.push({ + start: { line: lineIndex, character: charPos }, + end: { line: lineIndex, character: charPos + parts[i].length }, + properties: { class: className } + }); + } + charPos += parts[i].length; + } + return cleanLine; +} + +export function shikiDiffNotation(options: ShikiDiffNotationOptions = {}): ShikiTransformer { + const { + classLineAdd = 'add', + classLineRemove = 'remove', + classInlineAdd = 'inline-add', + classInlineRemove = 'inline-remove', + classActivePre = 'diff' + } = options; + + return { + name: 'shiki-diff-notation', + preprocess(code) { + let hasDiff = false; + const decorations: DecorationItem[] = []; + + const lines = code.split('\n').map((line, lineIndex) => { + // Whole-line additions/deletions: lines starting with + or - + // (the code hook strips the prefix and adds line classes) + if ( + (line.startsWith('+') && !line.startsWith('+++')) || + (line.startsWith('-') && !line.startsWith('---')) + ) { + hasDiff = true; + return line; + } + + // Inline: strip +++text+++ / ---text--- markers and record decorations. + // Process --- first so that +++ positions are computed against the + // already-stripped text (correct offsets when both appear on one line). + if (line.includes('---')) { + line = processInlineMarkers( + line, + lineIndex, + '---', + classInlineRemove, + decorations + ); + hasDiff = true; + } + if (line.includes('+++')) { + line = processInlineMarkers( + line, + lineIndex, + '+++', + classInlineAdd, + decorations + ); + hasDiff = true; + } + + return line; + }); + + if (hasDiff) { + (this.meta as Record).hasDiff = true; + if (decorations.length > 0) { + if (!this.options.decorations) this.options.decorations = []; + this.options.decorations.push(...decorations); + } + return lines.join('\n'); + } + }, + code(node) { + const active = + (this.meta as Record).hasDiff || this.options.meta?.diff; + if (!active) return; + this.addClassToHast(this.pre, classActivePre); + + for (const line of node.children) { + if (line.type !== 'element') continue; + for (const child of line.children) { + if (child.type !== 'element') continue; + const text = child.children[0]; + if (!text || text.type !== 'text') continue; + + if (text.value.startsWith('+')) { + text.value = text.value.slice(1); + this.addClassToHast(line, classLineAdd); + break; + } + if (text.value.startsWith('-')) { + text.value = text.value.slice(1); + this.addClassToHast(line, classLineRemove); + break; + } + } + } + } + }; +} diff --git a/src/lib/server/tutorial.ts b/src/lib/server/tutorial.ts new file mode 100644 index 000000000..c2f50a4d5 --- /dev/null +++ b/src/lib/server/tutorial.ts @@ -0,0 +1,274 @@ +import { marked, type Tokens } from 'marked'; +import { getSingletonHighlighter } from 'shiki'; +import { shikiDiffNotation, shikiFileHeader } from './shikiDiffNotation.js'; + +export interface ExerciseStub { + slug: string; + title: string; + chapter: string; + group: string; +} + +export interface Exercise { + slug: string; + title: string; + chapter: string; + group: string; + html: string; + /** starting file state: path → content, paths relative to /src/lib/ */ + a: Record; + /** solution file state (empty if no solution) */ + b: Record; + prev: ExerciseStub | null; + next: ExerciseStub | null; + focus: string; +} + +// Eagerly import all tutorial content at build time +const mds = import.meta.glob('/src/content/tutorial/**/*.md', { + query: '?raw', + import: 'default', + eager: true +}) as Record; + +const assets = import.meta.glob('/src/content/tutorial/**', { + query: '?raw', + import: 'default', + eager: true +}) as Record; + +function get_files(prefix: string): Record { + const result: Record = {}; + for (const [path, content] of Object.entries(assets)) { + if (path.startsWith(prefix) && !path.endsWith('.md')) { + result[path.slice(prefix.length)] = content as string; + } + } + return result; +} + +function parse_front_matter(md: string): { meta: Record; body: string } { + const match = md.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); + if (!match) return { meta: {}, body: md }; + const meta: Record = {}; + for (const line of match[1].split('\n')) { + const colon = line.indexOf(':'); + if (colon !== -1) meta[line.slice(0, colon).trim()] = line.slice(colon + 1).trim(); + } + return { meta, body: match[2] }; +} + +let hl_promise: ReturnType | null = null; + +function get_highlighter() { + if (!hl_promise) { + hl_promise = getSingletonHighlighter({ + themes: ['github-light', 'github-dark'], + langs: ['javascript', 'typescript', 'svelte', 'css', 'html', 'bash', 'json', 'text'] + }); + } + return hl_promise; +} + +let marked_configured = false; + +async function ensure_marked_configured() { + if (marked_configured) return; + const hl = await get_highlighter(); + marked.use({ + renderer: { + code({ text, lang }: Tokens.Code): string { + const language = lang?.toLowerCase() ?? ''; + if (language) { + try { + return hl.codeToHtml(text, { + lang: language, + themes: { light: 'github-light', dark: 'github-dark' }, + defaultColor: false, + transformers: [ + shikiFileHeader(), + shikiDiffNotation({ + classLineAdd: 'line--added', + classLineRemove: 'line--deleted' + }) + ] + }); + } catch { + // unknown language — fall through + } + } + const esc = (s: string) => + s.replace(/&/g, '&').replace(//g, '>'); + return `
    ${esc(text)}
    `; + } + } + }); + marked_configured = true; +} + +async function render_markdown(body: string): Promise { + await ensure_marked_configured(); + return (await marked(body)) as string; +} + +const TUTORIAL_ROOT = '/src/content/tutorial'; + +function sorted_keys(obj: Record, prefix: string) { + return Object.keys(obj) + .filter((k) => k.startsWith(prefix)) + .sort(); +} + +function group_label(dir: string): string { + return dir + .replace(/^\d+-/, '') + .replace(/-/g, ' ') + .replace(/\b\w/g, (c) => c.toUpperCase()); +} + +interface RawExercise { + slug: string; + title: string; + chapter: string; + group: string; + mdPath: string; + assetPrefix: string; + chapterAssetPrefix: string; + groupAssetPrefix: string; +} + +function build_exercise_list(): RawExercise[] { + const list: RawExercise[] = []; + + // Find all group dirs (e.g. 01-basics, 02-marks) — dirs that contain chapter subdirs + const groups = sorted_keys(mds, TUTORIAL_ROOT + '/').reduce((acc, p) => { + const rel = p.slice(TUTORIAL_ROOT.length + 1); + const parts = rel.split('/'); + if (parts.length >= 4 && parts[0] !== '+assets') { + if (!acc.includes(parts[0])) acc.push(parts[0]); + } + return acc; + }, [] as string[]); + + for (const group of groups) { + const groupBase = `${TUTORIAL_ROOT}/${group}`; + + // Chapter dirs within group + const chapters = sorted_keys(mds, groupBase + '/').reduce((acc, p) => { + const rel = p.slice(groupBase.length + 1); + const parts = rel.split('/'); + if (parts.length >= 3 && parts[0] !== '+assets' && parts[1] !== '+assets') { + if (!acc.includes(parts[0])) acc.push(parts[0]); + } + return acc; + }, [] as string[]); + + for (const chapter of chapters) { + const chapterBase = `${groupBase}/${chapter}`; + const chapterMeta = mds[`${chapterBase}/index.md`] ?? ''; + const { meta: cm } = parse_front_matter(chapterMeta); + const chapterTitle = cm.title ?? chapter; + + // Exercise dirs within chapter + const exercises = sorted_keys(mds, chapterBase + '/').reduce((acc, p) => { + const rel = p.slice(chapterBase.length + 1); + const parts = rel.split('/'); + if (parts.length >= 2 && parts[0] !== '+assets' && parts[0] !== 'index.md') { + if (!acc.includes(parts[0])) acc.push(parts[0]); + } + return acc; + }, [] as string[]); + + for (const ex of exercises) { + const mdPath = `${chapterBase}/${ex}/index.md`; + if (!mds[mdPath]) continue; + const { meta } = parse_front_matter(mds[mdPath]); + list.push({ + slug: `${chapter}/${ex}`, + title: meta.title ?? ex, + chapter: chapterTitle, + group: group_label(group), + mdPath, + assetPrefix: `${chapterBase}/${ex}/+assets/`, + chapterAssetPrefix: `${chapterBase}/+assets/`, + groupAssetPrefix: `${groupBase}/+assets/` + }); + } + } + } + + return list; +} + +const exercise_list = build_exercise_list(); +const exercise_map = new Map(exercise_list.map((e, i) => [e.slug, { ...e, index: i }])); + +export function get_exercise_stubs(): ExerciseStub[] { + return exercise_list.map(({ slug, title, chapter, group }) => ({ + slug, + title, + chapter, + group + })); +} + +export async function load_exercise(slug: string): Promise { + const entry = exercise_map.get(slug); + if (!entry) return null; + + const { meta, body } = parse_front_matter(mds[entry.mdPath]); + const index = entry.index; + + // Merge shared assets → chapter assets → exercise app-a + const shared = get_files(entry.groupAssetPrefix); + const chapter_shared = get_files(entry.chapterAssetPrefix); + const app_a_raw = get_files(`${entry.assetPrefix}app-a/`); + const app_b_raw = get_files(`${entry.assetPrefix}app-b/`); + + // The REPL uses files prefixed with /src/lib/ as editable files + // Filter to only src/lib/ files for the editor + function only_lib(files: Record) { + const out: Record = {}; + for (const [k, v] of Object.entries(files)) { + if (k.startsWith('src/lib/') || k.startsWith('src/routes/')) out[k] = v; + } + return out; + } + + // If app-a is absent, use the previous step's solved state (app-a + app-b) as + // the starting point — the REPL merges a and b, so this matches where prev ended. + const app_a_layer = + Object.keys(app_a_raw).length === 0 && index > 0 + ? (() => { + const prev = exercise_list[index - 1]; + const prev_a = get_files(`${prev.assetPrefix}app-a/`); + const prev_b = get_files(`${prev.assetPrefix}app-b/`); + return { ...only_lib(prev_a), ...only_lib(prev_b) }; + })() + : only_lib(app_a_raw); + + const a = { ...only_lib(shared), ...only_lib(chapter_shared), ...app_a_layer }; + const b = only_lib(app_b_raw); + + const prev = index > 0 ? exercise_list[index - 1] : null; + const next = index < exercise_list.length - 1 ? exercise_list[index + 1] : null; + + const focus = meta.focus ?? '/src/lib/App.svelte'; + + return { + slug, + title: meta.title ?? slug, + chapter: entry.chapter, + group: entry.group, + html: await render_markdown(body), + a, + b, + focus: focus.replace(/^\//, ''), + prev: prev + ? { slug: prev.slug, title: prev.title, chapter: prev.chapter, group: prev.group } + : null, + next: next + ? { slug: next.slug, title: next.title, chapter: next.chapter, group: next.group } + : null + }; +} diff --git a/src/routes/+page.md b/src/routes/+page.md index 1341ff659..5c7c199bf 100644 --- a/src/routes/+page.md +++ b/src/routes/+page.md @@ -4,120 +4,120 @@ description: The best visualizations are built with Svelte. heroImage: /logo.svg tagline: A Svelte-native visualization framework based on the layered grammar of graphics principles. actions: - - label: Getting started - type: primary - to: /getting-started - - label: Why SveltePlot? - to: /why-svelteplot - type: flat - - label: Examples - to: /examples - type: flat + - label: Getting started + type: primary + to: /getting-started + - label: Why SveltePlot? + to: /why-svelteplot + type: flat + - label: Examples + to: /examples + type: flat examples: - - area/smoothed-area - - area/density - - area/layered-density - - area/streamgraph - - area/violin - - area/ridgeline - - arrow/metro - - axis/datawrapper-ticks - - bar/faceted-bars - - box/box-x-faceted - - box/box-y - - box/box-y-facet - - brush/overview-detail - - cell/temperatures-threshold - - contour/volcano - - contour/weather - - contour/interactive - - contour/faceted - - contour/sampled - - custom/histogram-topline - - delaunay/delaunay-link - - delaunay/hull-species - - delaunay/voronoi-penguins - - delaunay/voronoi-mesh-walmart - - density/grouped - - density/filled - - difference/anomaly-baseline - - difference/apple-yoy - - difference/trade-balance - - dot/1-colored-scatterplot - - dot/beeswarm-bubbles - - dot/dot-faceted - - dot/dodge-faceted - - dot/walmart-stores - - geo/earthquakes - - geo/us-choropleth - - image/image-scatter - - image/image-beeswarm - - line/bls - - line/geo-line - - line/gradient-line - - line/indexed-stocks - - line/running-mean - - line/parallel-x - - line/penguins-cdf - - line/parallel-y-hl - - link/spherical-link - - raster/volcano - - raster/sampled - - raster/weather - - rect/binned - - rect/marimekko - - rect/marimekko-faceted - - rect/stacked-histogram - - regression/grouped - - regression/log - - rule/data-rules - - text/beeswarm-country-codes - - tick/tick-x - - trail/countries - - trail/napoleon - - trail/tdf - - vector/shift-map - - vector/spike-map - - vector/wind - - waffle/custom-symbol - - waffle/stacked-x + - area/smoothed-area + - area/density + - area/layered-density + - area/streamgraph + - area/violin + - area/ridgeline + - arrow/metro + - axis/datawrapper-ticks + - bar/faceted-bars + - box/box-x-faceted + - box/box-y + - box/box-y-facet + - brush/overview-detail + - cell/temperatures-threshold + - contour/volcano + - contour/weather + - contour/interactive + - contour/faceted + - contour/sampled + - custom/histogram-topline + - delaunay/delaunay-link + - delaunay/hull-species + - delaunay/voronoi-penguins + - delaunay/voronoi-mesh-walmart + - density/grouped + - density/filled + - difference/anomaly-baseline + - difference/apple-yoy + - difference/trade-balance + - dot/1-colored-scatterplot + - dot/beeswarm-bubbles + - dot/dot-faceted + - dot/dodge-faceted + - dot/walmart-stores + - geo/earthquakes + - geo/us-choropleth + - image/image-scatter + - image/image-beeswarm + - line/bls + - line/geo-line + - line/gradient-line + - line/indexed-stocks + - line/running-mean + - line/parallel-x + - line/penguins-cdf + - line/parallel-y-hl + - link/spherical-link + - raster/volcano + - raster/sampled + - raster/weather + - rect/binned + - rect/marimekko + - rect/marimekko-faceted + - rect/stacked-histogram + - regression/grouped + - regression/log + - rule/data-rules + - text/beeswarm-country-codes + - tick/tick-x + - trail/countries + - trail/napoleon + - trail/tdf + - vector/shift-map + - vector/spike-map + - vector/wind + - waffle/custom-symbol + - waffle/stacked-x _features: - - title: Marks - description: SveltePlot comes with a powerful set of built-in marks for building for your visualizations - icon: - type: iconify - collection: carbon - name: roadmap - - title: Automatic scales - description: Scale types and domains are automatically inferred from your data, unless you customize them - icon: - type: iconify - collection: ri - name: ruler-line - - title: Fully reactive - description: Everything in SveltePlot is fully reactive, the plot just updates when the data or configuration changes - icon: - type: iconify - collection: ri - name: svelte-line - - title: TypeScript - description: All components are fully typed and documented to integrate with VSCode - icon: - type: iconify - collection: nonicons - name: typescript-16 - - title: Customizable - description: All components are fully typed and documented to integrate with VSCode - icon: - type: iconify - collection: nonicons - name: typescript-16 - - title: Written in Svelte5 & TypeScript - description: All components and props are fully reactive, typed and documented! - icon: - type: iconify - collection: ri - name: svelte-line + - title: Marks + description: SveltePlot comes with a powerful set of built-in marks for building for your visualizations + icon: + type: iconify + collection: carbon + name: roadmap + - title: Automatic scales + description: Scale types and domains are automatically inferred from your data, unless you customize them + icon: + type: iconify + collection: ri + name: ruler-line + - title: Fully reactive + description: Everything in SveltePlot is fully reactive, the plot just updates when the data or configuration changes + icon: + type: iconify + collection: ri + name: svelte-line + - title: TypeScript + description: All components are fully typed and documented to integrate with VSCode + icon: + type: iconify + collection: nonicons + name: typescript-16 + - title: Customizable + description: All components are fully typed and documented to integrate with VSCode + icon: + type: iconify + collection: nonicons + name: typescript-16 + - title: Written in Svelte5 & TypeScript + description: All components and props are fully reactive, typed and documented! + icon: + type: iconify + collection: ri + name: svelte-line ---
    diff --git a/src/routes/api/marks/+page.md b/src/routes/api/marks/+page.md index 5c660e427..e26018659 100644 --- a/src/routes/api/marks/+page.md +++ b/src/routes/api/marks/+page.md @@ -1081,32 +1081,32 @@ These props are shared by marks via the base type aliases. ```ts export type StackOptions = - | { - offset: null | StackOffset; - order: null | StackOrder; - reverse: boolean; - } - | false; + | { + offset: null | StackOffset; + order: null | StackOrder; + reverse: boolean; + } + | false; ``` ### DodgeXOptions ```ts export type DodgeXOptions = - | AnchorX - | (BaseDodgeOptions & { - anchor?: 'left' | 'right' | 'middle'; - }); + | AnchorX + | (BaseDodgeOptions & { + anchor?: 'left' | 'right' | 'middle'; + }); ``` ### DodgeYOptions ```ts export type DodgeYOptions = - | AnchorY - | (BaseDodgeOptions & { - anchor?: 'top' | 'bottom' | 'middle'; - }); + | AnchorY + | (BaseDodgeOptions & { + anchor?: 'top' | 'bottom' | 'middle'; + }); ``` ### MarkerShape diff --git a/src/routes/api/plot/+page.md b/src/routes/api/plot/+page.md index 04e47bac1..406591e2f 100644 --- a/src/routes/api/plot/+page.md +++ b/src/routes/api/plot/+page.md @@ -62,18 +62,18 @@ The Plot component is the container for plots. It collects the marks with their ```ts export type XScaleOptions = ScaleOptions & { - grid: boolean; - axis: AxisXAnchor | false; - tickRotate: number; - labelAnchor: 'auto' | 'left' | 'center' | 'right'; - tickFormat: - | 'auto' - | false - | Intl.NumberFormatOptions - | Intl.DateTimeFormatOptions - | TickFormatFunction; - wordWrap: boolean; - removeDuplicateTicks: boolean; + grid: boolean; + axis: AxisXAnchor | false; + tickRotate: number; + labelAnchor: 'auto' | 'left' | 'center' | 'right'; + tickFormat: + | 'auto' + | false + | Intl.NumberFormatOptions + | Intl.DateTimeFormatOptions + | TickFormatFunction; + wordWrap: boolean; + removeDuplicateTicks: boolean; }; ``` @@ -81,16 +81,16 @@ export type XScaleOptions = ScaleOptions & { ```ts export type YScaleOptions = ScaleOptions & { - grid: boolean; - axis: AxisYAnchor | false; - tickFormat: - | 'auto' - | false - | Intl.NumberFormatOptions - | Intl.DateTimeFormatOptions - | TickFormatFunction; - tickRotate: number; - labelAnchor: 'auto' | 'bottom' | 'middle' | 'top'; + grid: boolean; + axis: AxisYAnchor | false; + tickFormat: + | 'auto' + | false + | Intl.NumberFormatOptions + | Intl.DateTimeFormatOptions + | TickFormatFunction; + tickRotate: number; + labelAnchor: 'auto' | 'bottom' | 'middle' | 'top'; }; ``` @@ -128,29 +128,29 @@ export type YScaleOptions = ScaleOptions & { ```ts export type ColorScaleOptions = ScaleOptions & { - legend: boolean; - type: - | ScaleType - | 'categorical' - | 'sequential' - | 'cyclical' - | 'threshold' - | 'quantile' - | 'quantize' - | 'diverging' - | 'diverging-log' - | 'diverging-pow' - | 'diverging-sqrt' - | 'diverging-symlog'; - scheme: ColorScheme | string[] | Record; - unknown: string; - pivot: number; - n: number; - interpolate: (d: any) => typeof d; - tickFormat: - | false - | Intl.NumberFormatOptions - | TickFormatFunction; + legend: boolean; + type: + | ScaleType + | 'categorical' + | 'sequential' + | 'cyclical' + | 'threshold' + | 'quantile' + | 'quantize' + | 'diverging' + | 'diverging-log' + | 'diverging-pow' + | 'diverging-sqrt' + | 'diverging-symlog'; + scheme: ColorScheme | string[] | Record; + unknown: string; + pivot: number; + n: number; + interpolate: (d: any) => typeof d; + tickFormat: + | false + | Intl.NumberFormatOptions + | TickFormatFunction; }; ``` @@ -158,7 +158,7 @@ export type ColorScaleOptions = ScaleOptions & { ```ts export type LegendScaleOptions = ScaleOptions & { - legend: boolean; + legend: boolean; }; ``` @@ -166,13 +166,13 @@ export type LegendScaleOptions = ScaleOptions & { ```ts export type ResolvedPlotOptions = Omit< - PlotOptions, - 'x' | 'y' | 'fx' | 'fy' + PlotOptions, + 'x' | 'y' | 'fx' | 'fy' > & { - x: ResolvedXScaleOptions; - y: ResolvedYScaleOptions; - fx: ResolvedFacetXOptions; - fy: ResolvedFacetYOptions; + x: ResolvedXScaleOptions; + y: ResolvedYScaleOptions; + fx: ResolvedFacetXOptions; + fy: ResolvedFacetYOptions; }; ``` @@ -192,9 +192,9 @@ export type PlotScales = Record; ```ts export type TickFormatFunction = ( - d: RawValue, - index: number, - ticks: RawValue[] + d: RawValue, + index: number, + ticks: RawValue[] ) => string | string[]; ``` @@ -280,8 +280,8 @@ Internal plot options shape after shorthand forms (`false`, arrays) are normaliz ```ts type ResolvedXScaleOptions = Partial & { - tickSpacing: number; - ticks?: RawValue[]; + tickSpacing: number; + ticks?: RawValue[]; }; ``` @@ -289,8 +289,8 @@ type ResolvedXScaleOptions = Partial & { ```ts type ResolvedYScaleOptions = Partial & { - tickSpacing: number; - ticks?: RawValue[]; + tickSpacing: number; + ticks?: RawValue[]; }; ``` @@ -298,10 +298,10 @@ type ResolvedYScaleOptions = Partial & { ```ts type ResolvedFacetXOptions = Partial & { - axisProps?: Partial>; - axisOptions?: Partial< - ComponentProps['options'] - >; + axisProps?: Partial>; + axisOptions?: Partial< + ComponentProps['options'] + >; }; ``` @@ -309,10 +309,10 @@ type ResolvedFacetXOptions = Partial & { ```ts type ResolvedFacetYOptions = Partial & { - axisProps?: Partial>; - axisOptions?: Partial< - ComponentProps['options'] - >; + axisProps?: Partial>; + axisOptions?: Partial< + ComponentProps['options'] + >; }; ``` @@ -350,12 +350,12 @@ Specific methods only exist for certain scale types and may be undefined. ```ts export type PlotScaleFunction = ((value: any) => any) & { - range: () => RawValue[]; - invert: (value: number) => RawValue; - bandwidth: () => number; - ticks: (count: number) => RawValue[]; - quantiles: () => number[]; - thresholds: () => number[]; - domain: () => RawValue[]; + range: () => RawValue[]; + invert: (value: number) => RawValue; + bandwidth: () => number; + ticks: (count: number) => RawValue[]; + quantiles: () => number[]; + thresholds: () => number[]; + domain: () => RawValue[]; }; ``` diff --git a/src/routes/api/transforms/+page.md b/src/routes/api/transforms/+page.md index 2982a14d1..2482cb726 100644 --- a/src/routes/api/transforms/+page.md +++ b/src/routes/api/transforms/+page.md @@ -54,38 +54,38 @@ type BinOptions = BinBaseOptions & AdditionalOutputChannels; ```ts type ReducerOption = - | ReducerName - | ((group: DataRecord[]) => RawValue); + | ReducerName + | ((group: DataRecord[]) => RawValue); ``` ### ReducerName ```ts export type ReducerName = - | 'count' - | 'deviation' - | 'difference' - | 'first' - | 'last' - | 'max' - | 'mean' - | 'median' - | 'min' - | 'mode' - | 'ratio' - | 'sum' - | 'variance' - | ReducerPercentile; + | 'count' + | 'deviation' + | 'difference' + | 'first' + | 'last' + | 'max' + | 'mean' + | 'median' + | 'min' + | 'mode' + | 'ratio' + | 'sum' + | 'variance' + | ReducerPercentile; ``` ### ReducerPercentile ```ts export type ReducerPercentile = - | (`p${Digit}${Digit}` & Record) - | 'p25' - | 'p50' - | 'p75'; + | (`p${Digit}${Digit}` & Record) + | 'p25' + | 'p50' + | 'p75'; ``` ## binX @@ -100,12 +100,12 @@ binX({ data, ...channels }: TransformArg, options: BinXOptions): Tra ```ts export type BinXOptions = BinBaseOptions & - AdditionalOutputChannels & - Partial<{ - y: ReducerOption; - y1: ReducerOption; - y2: ReducerOption; - }>; + AdditionalOutputChannels & + Partial<{ + y: ReducerOption; + y1: ReducerOption; + y2: ReducerOption; + }>; ``` Uses: [BinBaseOptions](/api/transforms#BinBaseOptions), [AdditionalOutputChannels](/api/transforms#AdditionalOutputChannels), [ReducerOption](/api/transforms#ReducerOption) @@ -122,12 +122,12 @@ binY({ data, ...channels }: TransformArg, options: BinYOptions): Tra ```ts export type BinYOptions = BinBaseOptions & - AdditionalOutputChannels & - Partial<{ - x: ReducerOption; - x1: ReducerOption; - x2: ReducerOption; - }>; + AdditionalOutputChannels & + Partial<{ + x: ReducerOption; + x1: ReducerOption; + x2: ReducerOption; + }>; ``` Uses: [BinBaseOptions](/api/transforms#BinBaseOptions), [AdditionalOutputChannels](/api/transforms#AdditionalOutputChannels), [ReducerOption](/api/transforms#ReducerOption) @@ -244,7 +244,7 @@ e.g. `{ x: 'rank' }` ```ts export type MapOptions = Partial< - Record + Record >; ``` @@ -254,11 +254,11 @@ a named map method, a custom mapping function, or a MapIndexObject ```ts export type MapMethod = - | 'cumsum' - | 'rank' - | 'quantile' - | ((I: number[], S: number[]) => number[]) - | MapIndexObject; + | 'cumsum' + | 'rank' + | 'quantile' + | ((I: number[], S: number[]) => number[]) + | MapIndexObject; ``` ### MapIndexObject @@ -275,9 +275,9 @@ a function that maps source values (S) to target values (T) for the given indice ```ts export type MapIndexFunction = ( - I: number[], - S: RawValue[], - T: RawValue[] + I: number[], + S: RawValue[], + T: RawValue[] ) => void; ``` @@ -309,27 +309,27 @@ normalizeX(args: TransformArg, options: NormalizeOptions): void ```ts type NormalizeOptions = - | NormalizeBasis - | { - basis: NormalizeBasis; - }; + | NormalizeBasis + | { + basis: NormalizeBasis; + }; ``` ### NormalizeBasis ```ts type NormalizeBasis = - | 'deviation' - | 'first' - | 'last' - | 'min' - | 'max' - | 'mean' - | 'median' - | 'sum' - | 'extent' - | BasisFunction - | MapIndexObject; + | 'deviation' + | 'first' + | 'last' + | 'min' + | 'max' + | 'mean' + | 'median' + | 'sum' + | 'extent' + | BasisFunction + | MapIndexObject; ``` Uses: [MapIndexObject](/api/transforms#MapIndexObject) @@ -391,13 +391,13 @@ group({ data, ...channels }: TransformArg, options: GroupXOptions): ```ts type GroupXOptions = GroupBaseOptions & - AdditionalOutputChannels & - Partial<{ - y: ReducerOption; - y1: ReducerOption; - y2: ReducerOption; - xPropName: string; - }>; + AdditionalOutputChannels & + Partial<{ + y: ReducerOption; + y1: ReducerOption; + y2: ReducerOption; + xPropName: string; + }>; ``` Uses: [AdditionalOutputChannels](/api/transforms#AdditionalOutputChannels), [ReducerOption](/api/transforms#ReducerOption) @@ -439,13 +439,13 @@ groupY(input: TransformArg, options: GroupYOptions): void ```ts type GroupYOptions = GroupBaseOptions & - AdditionalOutputChannels & - Partial<{ - x: ReducerOption; - x1: ReducerOption; - x2: ReducerOption; - yPropName: string; - }>; + AdditionalOutputChannels & + Partial<{ + x: ReducerOption; + x1: ReducerOption; + x2: ReducerOption; + yPropName: string; + }>; ``` Uses: [GroupBaseOptions](/api/transforms#GroupBaseOptions), [AdditionalOutputChannels](/api/transforms#AdditionalOutputChannels), [ReducerOption](/api/transforms#ReducerOption) @@ -496,16 +496,16 @@ jitter({ data, ...channels }: TransformArg, options: Partial number; + source?: () => number; } & ( - | { - type: 'uniform'; - width?: number | string; - } - | { - type: 'normal'; - std?: number | string; - } + | { + type: 'uniform'; + width?: number | string; + } + | { + type: 'normal'; + std?: number | string; + } ); ``` @@ -559,7 +559,7 @@ renameChannels>({ data, ...chan ```ts type RenameChannelsOptions = Partial< - Record + Record >; ``` @@ -576,7 +576,7 @@ replaceChannels>({ data, ...cha ```ts type ReplaceChannelsOptions = Partial< - Record + Record >; ``` @@ -593,21 +593,21 @@ select({ data, ...channels }: TransformArg, options: SelectOptions): ```ts type SelectOptions = - | 'first' - | 'last' - | AtLeastOne<{ - [k in ChannelName]: 'min' | 'max'; - }>; + | 'first' + | 'last' + | AtLeastOne<{ + [k in ChannelName]: 'min' | 'max'; + }>; ``` ### AtLeastOne ```ts type AtLeastOne< - T, - U = { - [K in keyof T]: Pick; - } + T, + U = { + [K in keyof T]: Pick; + } > = Partial & U[keyof U]; ``` @@ -673,7 +673,7 @@ per-channel shift amounts for x channels; values can be numbers or time interval ```ts type ShiftXOptions = { - [key in 'x' | 'x1' | 'x2']: string | number; + [key in 'x' | 'x1' | 'x2']: string | number; }; ``` @@ -691,7 +691,7 @@ per-channel shift amounts for y channels; values can be numbers or time interval ```ts type ShiftYOptions = { - [key in 'y' | 'y1' | 'y2']: string | number; + [key in 'y' | 'y1' | 'y2']: string | number; }; ``` @@ -738,12 +738,12 @@ options for the stack transform, or false to disable stacking ```ts export type StackOptions = - | { - offset: null | StackOffset; - order: null | StackOrder; - reverse: boolean; - } - | false; + | { + offset: null | StackOffset; + order: null | StackOrder; + reverse: boolean; + } + | false; ``` ### StackOffset @@ -837,8 +837,8 @@ a generic record type used when the specific mark options type is not known ```ts export type GenericMarkOptions = Record< - string | symbol, - any + string | symbol, + any >; ``` @@ -881,13 +881,13 @@ the name of a d3 curve interpolation method ```ts export type ConstantAccessor< - K, - T = Record + K, + T = Record > = - | K - | BivariantCallback<[d: T, index: number], K> - | null - | undefined; + | K + | BivariantCallback<[d: T, index: number], K> + | null + | undefined; ``` ### TransformArg @@ -896,9 +896,9 @@ the input argument to a data transform: data array plus channel mappings and mar ```ts export type TransformArg = Channels & - BaseMarkProps & { - data: T[]; - }; + BaseMarkProps & { + data: T[]; + }; ``` ### MapArg @@ -907,7 +907,7 @@ the input argument to a map transform: data array plus channel mappings ```ts export type MapArg = Channels & { - data: T[]; + data: T[]; }; ``` @@ -917,9 +917,9 @@ transform input for raw data rows (before recordization) ```ts export type TransformArgsRow = - Partial> & { - data: T[]; - }; + Partial> & { + data: T[]; + }; ``` ### TransformArgsRecord @@ -928,9 +928,9 @@ transform input for data records (after recordization) ```ts export type TransformArgsRecord = Partial< - Channels + Channels > & { - data: T[]; + data: T[]; }; ``` @@ -940,8 +940,8 @@ the return type of a transform, ensuring data is always present ```ts export type TransformReturn< - C extends TransformArg, - T + C extends TransformArg, + T > = C & Required, 'data'>>; ``` diff --git a/src/routes/examples/axis/rug-plot.svelte b/src/routes/examples/axis/rug-plot.svelte new file mode 100644 index 000000000..a049ea0a8 --- /dev/null +++ b/src/routes/examples/axis/rug-plot.svelte @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + d.body_mass_g)} + text={false} /> + + diff --git a/src/routes/examples/brush/constrained.svelte b/src/routes/examples/brush/constrained.svelte index 98e5fde56..15b96b8de 100644 --- a/src/routes/examples/brush/constrained.svelte +++ b/src/routes/examples/brush/constrained.svelte @@ -19,8 +19,8 @@ (brush.enabled ? 'gray' : d.species)} symbol="species" /> @@ -29,12 +29,12 @@ - d.culmen_length_mm >= brush.x1 && - d.culmen_length_mm <= brush.x2 && - d.culmen_depth_mm >= brush.y1 && - d.culmen_depth_mm <= brush.y2} - x="culmen_length_mm" - y="culmen_depth_mm" + d.bill_length_mm >= brush.x1 && + d.bill_length_mm <= brush.x2 && + d.bill_depth_mm >= brush.y1 && + d.bill_depth_mm <= brush.y2} + x="bill_length_mm" + y="bill_depth_mm" stroke="species" symbol="species" /> {/if} diff --git a/src/routes/examples/brush/filter.svelte b/src/routes/examples/brush/filter.svelte index 37182032a..1ab7de4e3 100644 --- a/src/routes/examples/brush/filter.svelte +++ b/src/routes/examples/brush/filter.svelte @@ -19,8 +19,8 @@ (brush.enabled ? 'gray' : d.species)} symbol="species" /> @@ -29,12 +29,12 @@ - d.culmen_length_mm >= brush.x1 && - d.culmen_length_mm <= brush.x2 && - d.culmen_depth_mm >= brush.y1 && - d.culmen_depth_mm <= brush.y2} - x="culmen_length_mm" - y="culmen_depth_mm" + d.bill_length_mm >= brush.x1 && + d.bill_length_mm <= brush.x2 && + d.bill_depth_mm >= brush.y1 && + d.bill_depth_mm <= brush.y2} + x="bill_length_mm" + y="bill_depth_mm" stroke="species" symbol="species" /> {/if} diff --git a/src/routes/examples/brush/zoomable-scatter.svelte b/src/routes/examples/brush/zoomable-scatter.svelte index f266995d1..b080eb565 100644 --- a/src/routes/examples/brush/zoomable-scatter.svelte +++ b/src/routes/examples/brush/zoomable-scatter.svelte @@ -11,8 +11,8 @@ import { extent } from 'd3-array'; type PenguinRow = { - culmen_length_mm: number; - culmen_depth_mm: number; + bill_length_mm: number; + bill_depth_mm: number; species: string; }; @@ -25,11 +25,11 @@ const fullDomainX = extent( penguins, - (d) => d.culmen_length_mm + (d) => d.bill_length_mm ); const fullDomainY = extent( penguins, - (d) => d.culmen_depth_mm + (d) => d.bill_depth_mm ); let domainX: [number, number] | [undefined, undefined] = @@ -56,16 +56,16 @@ grid x={{ domain: domainXT.current as any, - label: 'culmen_length_mm' + label: 'bill_length_mm' }} y={{ domain: domainYT.current as any, - label: 'culmen_depth_mm' + label: 'bill_depth_mm' }}> {#if !isZoomedIn} diff --git a/src/routes/examples/contour/faceted.svelte b/src/routes/examples/contour/faceted.svelte index 78fe77eaf..927820f82 100644 --- a/src/routes/examples/contour/faceted.svelte +++ b/src/routes/examples/contour/faceted.svelte @@ -25,8 +25,8 @@ }}> diff --git a/src/routes/examples/custom/multiple.svelte b/src/routes/examples/custom/multiple.svelte index 6fba18eba..5520af2e2 100644 --- a/src/routes/examples/custom/multiple.svelte +++ b/src/routes/examples/custom/multiple.svelte @@ -30,8 +30,8 @@ {#snippet mark({ record })} diff --git a/src/routes/examples/custom/single.svelte b/src/routes/examples/custom/single.svelte index 818aca35e..efc951a2c 100644 --- a/src/routes/examples/custom/single.svelte +++ b/src/routes/examples/custom/single.svelte @@ -30,7 +30,7 @@ diff --git a/src/routes/examples/delaunay/delaunay-link.svelte b/src/routes/examples/delaunay/delaunay-link.svelte index a0d3bb1ed..a537865ba 100644 --- a/src/routes/examples/delaunay/delaunay-link.svelte +++ b/src/routes/examples/delaunay/delaunay-link.svelte @@ -16,14 +16,14 @@ diff --git a/src/routes/examples/delaunay/delaunay-mesh-grouped.svelte b/src/routes/examples/delaunay/delaunay-mesh-grouped.svelte index 3f9535b4e..eac4f68b4 100644 --- a/src/routes/examples/delaunay/delaunay-mesh-grouped.svelte +++ b/src/routes/examples/delaunay/delaunay-mesh-grouped.svelte @@ -16,15 +16,15 @@ diff --git a/src/routes/examples/delaunay/delaunay-mesh.svelte b/src/routes/examples/delaunay/delaunay-mesh.svelte index 10906569b..d17dbfccd 100644 --- a/src/routes/examples/delaunay/delaunay-mesh.svelte +++ b/src/routes/examples/delaunay/delaunay-mesh.svelte @@ -16,13 +16,13 @@ diff --git a/src/routes/examples/delaunay/hull-blur.svelte b/src/routes/examples/delaunay/hull-blur.svelte new file mode 100644 index 000000000..f269f773e --- /dev/null +++ b/src/routes/examples/delaunay/hull-blur.svelte @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/src/routes/examples/delaunay/hull-species.svelte b/src/routes/examples/delaunay/hull-species.svelte index 4771229a8..4db00dbe3 100644 --- a/src/routes/examples/delaunay/hull-species.svelte +++ b/src/routes/examples/delaunay/hull-species.svelte @@ -3,6 +3,7 @@ export const description = 'Convex hulls drawn around each penguin species cluster, with fill and stroke colored by species.'; export const data = { penguins: '/data/penguins.csv' }; + export const sortKey = 10; + + + + + + + diff --git a/src/routes/examples/line/gradient-line.svelte b/src/routes/examples/line/gradient-line.svelte index 90901ce55..d79d8ccad 100644 --- a/src/routes/examples/line/gradient-line.svelte +++ b/src/routes/examples/line/gradient-line.svelte @@ -1,5 +1,7 @@ diff --git a/src/routes/examples/pointer/faceted-1.svelte b/src/routes/examples/pointer/faceted-1.svelte index bec23b68d..8c777c0b0 100644 --- a/src/routes/examples/pointer/faceted-1.svelte +++ b/src/routes/examples/pointer/faceted-1.svelte @@ -14,22 +14,22 @@ + y="bill_depth_mm" /> {#snippet children({ data })} diff --git a/src/routes/examples/raster/sampled.svelte b/src/routes/examples/raster/sampled.svelte index b3b20a616..cc68aa289 100644 --- a/src/routes/examples/raster/sampled.svelte +++ b/src/routes/examples/raster/sampled.svelte @@ -1,7 +1,7 @@ diff --git a/src/routes/examples/regression/faceted.svelte b/src/routes/examples/regression/faceted.svelte index 976c92762..4d4836a90 100644 --- a/src/routes/examples/regression/faceted.svelte +++ b/src/routes/examples/regression/faceted.svelte @@ -14,20 +14,20 @@ diff --git a/src/routes/examples/regression/grouped.svelte b/src/routes/examples/regression/grouped.svelte index 93ac68d7f..0a1a21004 100644 --- a/src/routes/examples/regression/grouped.svelte +++ b/src/routes/examples/regression/grouped.svelte @@ -14,16 +14,16 @@ + x="bill_length_mm" + y="bill_depth_mm" /> diff --git a/src/routes/examples/rule/mean-summary.svelte b/src/routes/examples/rule/mean-summary.svelte index 3e97bac04..064d71020 100644 --- a/src/routes/examples/rule/mean-summary.svelte +++ b/src/routes/examples/rule/mean-summary.svelte @@ -14,15 +14,15 @@ + export const title = 'Faceted barcode'; + export const description = `Based on an example from Observable Plot, this chart shows the age distribution of the population in each US state as horizontal ticks. The x position of each tick represents the share of the state's population in that age group.`; + export const data = { penguins: '/data/penguins.csv' }; + + + + + + + diff --git a/src/routes/examples/types.ts b/src/routes/examples/types.ts index 895a48995..29c6e9775 100644 --- a/src/routes/examples/types.ts +++ b/src/routes/examples/types.ts @@ -21,8 +21,8 @@ export type SimpsonsRow = { export type PenguinsRow = { species: string; island: string; - culmen_length_mm: number; - culmen_depth_mm: number; + bill_depth_mm: number; + bill_length_mm: number; flipper_length_mm: number; body_mass_g: number; sex: 'MALE' | 'FEMALE' | ''; diff --git a/src/routes/features/defaults/+page.md b/src/routes/features/defaults/+page.md index 7f1cf6d85..50808eb7a 100644 --- a/src/routes/features/defaults/+page.md +++ b/src/routes/features/defaults/+page.md @@ -10,16 +10,16 @@ If you're using SveltePlot in a SvelteKit project, you can even set the defaults ```svelte title="+layout.svelte" @@ -101,63 +101,63 @@ Setting Global and Component Defaults ```svelte live + import { Plot, Dot, setPlotDefaults } from 'svelteplot'; + import { page } from '$app/state'; - - - -``` + let { penguins } = $derived(page.data.data); -```js -import { setPlotDefaults } from 'svelteplot'; - -setPlotDefaults({ + setPlotDefaults({ // Global defaults inset: 15, categoricalColorScheme: [ - 'var(--svp-red)', - 'var(--svp-blue)', - 'var(--svp-green)' + 'var(--svp-red)', + 'var(--svp-blue)', + 'var(--svp-green)' ], - // Mark-specific defaults + // Component-specific defaults axis: { - tickSize: 0, - tickPadding: 5 + tickSize: 0, + tickPadding: 5 }, frame: true, grid: { implicit: true }, dot: { - r: 5 + r: 5 } + }); + + + + + +``` + +```js +import { setPlotDefaults } from 'svelteplot'; + +setPlotDefaults({ + // Global defaults + inset: 15, + categoricalColorScheme: [ + 'var(--svp-red)', + 'var(--svp-blue)', + 'var(--svp-green)' + ], + // Mark-specific defaults + axis: { + tickSize: 0, + tickPadding: 5 + }, + frame: true, + grid: { implicit: true }, + dot: { + r: 5 + } }); ``` @@ -165,35 +165,35 @@ another one ```svelte live
    - - - - - - + + + + + +
    ``` @@ -201,15 +201,15 @@ another one import { setPlotDefaults } from 'svelteplot'; setPlotDefaults({ - bar: { - borderRadius: 4, - stroke: 'currentColor', - fill: null - }, - area: { - curve: 'basis', - fillOpacity: 0.5 - } + bar: { + borderRadius: 4, + stroke: 'currentColor', + fill: null + }, + area: { + curve: 'basis', + fillOpacity: 0.5 + } }); ``` diff --git a/src/routes/features/facets/+page.md b/src/routes/features/facets/+page.md index f6a7fca81..0c65ae839 100644 --- a/src/routes/features/facets/+page.md +++ b/src/routes/features/facets/+page.md @@ -2,54 +2,52 @@ title: Facets --- -Facets are a way to split a plot into multiple panels. +Facets are a way to split a plot into multiple panels along one or two dimenstions. Each panel shows a subset of the data, making it easier to compare groups. ```svelte live -{#if penguins.length} - - - - -{/if} + + + + ``` ```svelte - - + + ``` @@ -59,29 +57,29 @@ Here's a histogram of Olympian athlete weights faceted by sex: ```svelte live - - + + ``` ```svelte - - + + ``` @@ -89,34 +87,34 @@ Here's a histogram of Olympian athlete weights faceted by sex: Apply top-level facet options automatically: -```svelte --live +```svelte live {#if penguins.length} - - - - - + + + + + {/if} ``` @@ -124,37 +122,48 @@ Bar chart facets ```svelte live - (d.year === 2021 ? 10 : -10)} - fx="party" - fill="party" - opacity="year" /> + x={{ label: ' ' }} + y={{ label: '' }} + opacity={{ range: [0.4, 1] }} + fx={{ + axis: 'bottom', + axisProps: { fontWeight: 'bold', tickFontSize: 13 }, + axisOptions: { dy: 20 } + }}> + (d.year === 2021 ? 10 : -10)} + fx="party" + fill="party" + opacity="year" /> ``` + +## Facet options + +You can pass options to the facet layout via the `facet` prop on ``. It accepts an object with the following properties: + +- `data`: the dataset to facet by (required) +- `x`: the column to facet by horizontally (optional) +- `y`: the column to facet by vertically (optional) +- `axis`: which axis to show for facet labels (`'top'`, `'bottom', `'left'`, `'right'`, or `false`) (optional) +- `axisProps`: props to pass to the facet axis (optional) +- `axisOptions`: options to pass to the facet axis (optional) diff --git a/src/routes/features/gradients/+page.md b/src/routes/features/gradients/+page.md index d640d47b5..1bfa0cd30 100644 --- a/src/routes/features/gradients/+page.md +++ b/src/routes/features/gradients/+page.md @@ -15,70 +15,70 @@ For horizontal gradients (= from left to right) ```svelte live - - - - + + + + ``` ```svelte - - - - + + + + ``` @@ -95,51 +95,51 @@ For vertical gradients (from top to bottom) ```svelte live - - - - + + + + ``` ```svelte - - - - + + + + ``` diff --git a/src/routes/features/interactivity/+page.md b/src/routes/features/interactivity/+page.md index 322b060e0..2f3d58e6c 100644 --- a/src/routes/features/interactivity/+page.md +++ b/src/routes/features/interactivity/+page.md @@ -10,30 +10,27 @@ SveltePlot components support standard DOM events like `onclick`, `onmouseover`, ```svelte live - - !clicked || clicked === d ? 1 : 0.5 - }} - onclick={(event, d) => { - clicked = d; - }} /> - + (!clicked || clicked === d ? 1 : 0.5) + }} + onclick={(event, d) => { + clicked = d; + }} /> + ``` @@ -43,87 +40,87 @@ The `HTMLTooltip` mark allows you to display HTML content when users hover over ```svelte live - - {#snippet overlay()} - - {#snippet children({ datum })} -
    -
    Species: {datum.species}
    -
    Island: {datum.island}
    -
    - {/snippet} -
    - {/snippet} + + {#snippet overlay()} + + {#snippet children({ datum })} +
    +
    Species: {datum.species}
    +
    Island: {datum.island}
    +
    + {/snippet} +
    + {/snippet}
    ``` ```svelte - - {#snippet overlay()} - - {#snippet children({ datum })} -
    -
    Species: {datum.species}
    -
    Island: {datum.island}
    -
    - {/snippet} -
    - {/snippet} + + {#snippet overlay()} + + {#snippet children({ datum })} +
    +
    Species: {datum.species}
    +
    Island: {datum.island}
    +
    + {/snippet} +
    + {/snippet}
    ``` @@ -133,39 +130,35 @@ SVG tooltips are created using the [Pointer](/marks/pointer) mark, which doesn't ```svelte live - - + + {#snippet children({ data })} + - {#snippet children({ data })} - d.Close.toFixed()} - lineAnchor="bottom" - fontWeight="bold" - dy="-5" /> - - {/snippet} - + text={(d) => d.Close.toFixed()} + lineAnchor="bottom" + fontWeight="bold" + dy="-5" /> + + {/snippet} + ``` @@ -175,21 +168,21 @@ The [Brush](/marks/brush) mark allows users to select data by dragging a rectang ```svelte live - - + + ``` @@ -201,38 +194,34 @@ Crosshairs provide a way to highlight data across dimensions. In SveltePlot, you ```svelte live - - - - - {#snippet children({ data })} - - - d.Date)} - tickFormat={(d) => d.getFullYear()} /> - d.Close)} - tickFormat={(d) => d.toFixed()} /> - {/snippet} - + + + + + {#snippet children({ data })} + + + d.Date)} + tickFormat={(d) => d.getFullYear()} /> + d.Close)} + tickFormat={(d) => d.toFixed()} /> + {/snippet} + ``` diff --git a/src/routes/features/markers/+page.md b/src/routes/features/markers/+page.md index 9dbed34cc..af19442de 100644 --- a/src/routes/features/markers/+page.md +++ b/src/routes/features/markers/+page.md @@ -6,65 +6,65 @@ Markers can be used with the [line](/marks/line) and [link](/marks/link) marks. ```svelte live + bind:value={projection} + {options} + label="Projection" /> + onclick={() => { + vx = 10; + step(); + }}>spin - - 50 ? 10 : zoom > 20 ? 5 : 1} /> - - d.geometry.coordinates[0]} - y={(d) => d.geometry.coordinates[1]} - r={(d) => Math.pow(10, d.properties.mag)} /> + r={{ range: [0.5, 25] }} + projection={{ + type: projection, + inset: 10, + domain: geoCircle() + .center([longitude, latitude]) + .radius(zoom)(), + rotate: [-longitude, -latitude] + }}> + + 50 ? 10 : zoom > 20 ? 5 : 1} /> + + d.geometry.coordinates[0]} + y={(d) => d.geometry.coordinates[1]} + r={(d) => Math.pow(10, d.properties.mag)} /> ``` ```svelte - (dragging = true)} - onmousemove={(d, evt) => { - if (dragging) { - latitude = Math.round( - latitude + evt.movementY - ); - longitude = Math.round( - longitude - evt.movementX - ); - } - }} - onmouseup={() => (dragging = false)} /> - - - Math.pow(10, d.mag)} /> + r={{ range: [0.5, 25] }} + projection={{ + type: 'orthographic', + inset: 10, + rotate: [-longitude, -latitude] + }}> + (dragging = true)} + onmousemove={(d, evt) => { + if (dragging) { + latitude = Math.round(latitude + evt.movementY); + longitude = Math.round(longitude - evt.movementX); + } + }} + onmouseup={() => (dragging = false)} /> + + + Math.pow(10, d.mag)} /> ``` @@ -153,115 +149,103 @@ Plot provides a variety of built-in projections. And as above, all world project ```svelte live + label="scheme" + options={schemes} + bind:value={scheme} /> - + grid + color={{ legend: true, scheme }} + y={{ type: 'log' }} + r={{ range: [2, 14] }}> + ``` ```svelte - + grid + y={{ type: 'log' }} + r={{ range: [2, 14] }} + color={{ legend: true, scheme: 'observable10' }}> + ``` @@ -712,41 +712,41 @@ If you want to map custom colors to your data, you need to pass them via the `sc ```svelte live - + grid + color={{ + legend: true, + scheme: [ + 'var(--svp-red)', + 'var(--svp-blue)', + 'var(--svp-green)' + ] + }}> + ``` ```svelte - + grid + color={{ + legend: true, + scheme: ['red', 'blue', 'green'] + }}> + ``` @@ -754,39 +754,39 @@ Note that the colors are picked in the order the categories appear in your datas ```svelte live - + grid + color={{ + legend: true, + domain: ['FEMALE', 'MALE'], + scheme: ['var(--svp-green)', 'var(--svp-violet)'] + }}> + ``` ```svelte - + grid + color={{ + legend: true, + domain: ['FEMALE', 'MALE'], + scheme: ['green', 'violet'] + }}> + ``` @@ -796,43 +796,43 @@ As a simpler syntax you can also pass domain -> scheme mapping as object: ```svelte live - + grid + color={{ + legend: true, + scheme: { + FEMALE: 'var(--svp-green)', + MALE: 'var(--svp-violet)' + } + }}> + ``` ```svelte - + grid + color={{ + legend: true, + scheme: { + FEMALE: 'green', + MALE: 'violet' + } + }}> + ``` @@ -846,49 +846,49 @@ Another very common way to color plots is to map numbers to colors. If you simpl ```svelte live - Intl.NumberFormat('en-US', { - style: 'unit', - unit: 'kilogram' - }).format(d / 1000) - }}> - + grid + height={200} + color={{ + legend: true, + label: 'Body mass', + tickFormat: (d) => + Intl.NumberFormat('en-US', { + style: 'unit', + unit: 'kilogram' + }).format(d / 1000) + }}> + ``` ```svelte - Intl.NumberFormat('en-US', { - style: 'unit', - unit: 'kilogram' - }).format(d / 1000) - }}> - + grid + height={200} + color={{ + legend: true, + label: 'Body mass', + tickFormat: (d) => + Intl.NumberFormat('en-US', { + style: 'unit', + unit: 'kilogram' + }).format(d / 1000) + }}> + ``` @@ -896,81 +896,81 @@ The default color scheme shown above is called `turbo`, but we can change it to ```svelte live + type="text" + bind:value={domain_raw} + pattern="^(-?\d+(?:\.\d+)?)(,(-?\d+(?:\.\d+)?))*$" /> - d.date.getUTCFullYear() === 2015} - x={(d) => d.date.getUTCDate()} - y={(d) => d.date.getUTCMonth()} - fill="temp_max" - inset="0.5" /> + padding={0} + aspectRatio={1} + color={{ + scheme: 'OrRd', + type: 'threshold', + domain, + legend: true + }} + y={{ + ticks: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + tickFormat: formatMonth('en', 'short') + }} + testid="seattle-temp"> + d.date.getUTCFullYear() === 2015} + x={(d) => d.date.getUTCDate()} + y={(d) => d.date.getUTCMonth()} + fill="temp_max" + inset="0.5" /> ``` @@ -1460,27 +1460,27 @@ The color scales are used whenever a _fill_ or _stroke_ channel is mapped to val ```svelte live - + ``` ```svelte - + ``` @@ -1488,45 +1488,45 @@ Note that SveltePlot also recognizes generic CSS variables as valid color names, ```svelte live - + ``` ```svelte - + ``` @@ -1534,38 +1534,38 @@ You can also force SveltePlot to bypass the color scale using `scale: null`. ```svelte live - - - - - - - + + + + + + + ``` ```svelte - - - - - - - + + + + + + + ``` diff --git a/src/routes/features/transforms/+page.md b/src/routes/features/transforms/+page.md index 4075f5f6b..aaa0d459d 100644 --- a/src/routes/features/transforms/+page.md +++ b/src/routes/features/transforms/+page.md @@ -28,38 +28,38 @@ In the returned `data` array, each item will have new properties `__y1` and `__y ```svelte --live - - + + ``` @@ -67,19 +67,19 @@ More text here ```svelte - - + + ``` diff --git a/src/routes/getting-started/+page.md b/src/routes/getting-started/+page.md index c6b75435a..7d14589e7 100644 --- a/src/routes/getting-started/+page.md +++ b/src/routes/getting-started/+page.md @@ -13,36 +13,33 @@ You can use SveltePlot inside any platform that supports Svelte 5, such as [Stac ```svelte live - + ``` ```svelte - + ``` @@ -62,21 +59,21 @@ pnpm add svelteplot ```svelte - + ``` ```svelte live - + ``` diff --git a/src/routes/introduction/+page.md b/src/routes/introduction/+page.md index 79cd0aba4..a4ea36ccc 100644 --- a/src/routes/introduction/+page.md +++ b/src/routes/introduction/+page.md @@ -30,7 +30,7 @@ If we want to plot a line showing the closing price over time, all we have to wr ```svelte - + ``` @@ -38,13 +38,13 @@ If we want to plot a line showing the closing price over time, all we have to wr ```svelte live - + ``` @@ -58,21 +58,21 @@ Let's say we also want to add a grid and a horizontal rule at zero. To activate ```svelte - - + + ``` ```svelte live - - + + ``` @@ -84,23 +84,23 @@ Now, let's also fill the area between the line and the horizontal rule by adding ```svelte - - - + + + ``` ```svelte live - - - + + + ``` @@ -110,59 +110,59 @@ Since SveltePlots are just SVG, you can mix in SVG elements. Let's say we want t ```svelte - - - - - - - - - + + + + + + + + + ``` ```svelte live - - - - - - - - - + + + + + + + + + ``` @@ -176,33 +176,33 @@ Our dataset contains daily data, but what if we want to show monthly aggregates ```svelte - - - - + + + + ``` ```svelte live - - + + ``` @@ -212,43 +212,43 @@ We can also use the binX transform to compute the min and max closing value of e ```svelte - - + + ``` ```svelte live - - + + ``` @@ -262,19 +262,19 @@ Also, somehow, these axes already know the extent of our data! This is possible ```svelte - + ``` ```svelte live - + ``` @@ -282,19 +282,19 @@ Similarly, you can enable the implicit grids by setting `grid` to `true`: ```svelte - + ``` ```svelte live - + ``` @@ -302,41 +302,41 @@ And somehow these axes already know the extent of the data we passed to the line ```svelte --live - + testid="olympians" + grid + color={{ legend: true }} + x={{ type: 'linear', insetLeft: 30, grid: true }} + inset={10}> + ``` ```svelte - + grid + color={{ legend: true }} + x={{ type: 'linear', insetLeft: 30, grid: true }} + inset={10}> + ``` @@ -344,28 +344,28 @@ This scatterplot suffers from overplotting: many dots are drawn in the same spot ```svelte --live {#if olympians} - - - + + + {/if} ``` @@ -373,46 +373,46 @@ We can use the [binX transform](/transforms/bin) to compute a weight distributio ```svelte --live {#if olympians} - - - - + + + + {/if} ``` ```svelte - - + + ``` @@ -420,52 +420,52 @@ Or we can use the built-in [faceting](/features/facets) to look at the distribut ```svelte --live {#if olympians} - - - - + + + + {/if} ``` ```svelte - - + + ``` diff --git a/src/routes/marks/area/+page.md b/src/routes/marks/area/+page.md index a044a4035..e1f36d76c 100644 --- a/src/routes/marks/area/+page.md +++ b/src/routes/marks/area/+page.md @@ -1,5 +1,15 @@ --- title: Area mark +description: AreaY and AreaX fill the region between a baseline and a line — used for time-series areas, stacked charts, and distributions. +examples: + - area/area-y + - area/streamgraph + - area/ridgeline + - area/violin + - area/stacked-density +links: + examples: /examples/area + api: /api/marks#AreaY --- - + ``` ```svelte - + ``` @@ -43,38 +53,28 @@ If you supply `undefined` values, the area mark will create gaps in the visualiz ```svelte live - + ``` ```svelte - + ``` @@ -82,41 +82,31 @@ In order to interpolate across undefined values you need to filter them, e.g. us ```svelte live - { - console.log(d); - return d !== undefined; - }} - canvas={$useCanvas} - data={[ - 1.5, - 2, - 3.5, - 4, - 5.5, - undefined, - 7, - 8.5, - 9 - ]} /> + { + console.log(d); + return d !== undefined; + }} + canvas={$useCanvas} + data={[1.5, 2, 3.5, 4, 5.5, undefined, 7, 8.5, 9]} /> ``` ```svelte - d !== undefined} /> + d !== undefined} /> ``` @@ -124,9 +114,9 @@ The Area mark supports **canvas rendering** by passing setting the `canvas` prop ```svelte live @@ -141,26 +131,26 @@ If you need a different baseline you can pass y1 and y2 channels i ```svelte live - + ``` ```svelte - + ``` @@ -175,31 +165,31 @@ You can also just pass an array of numbers to AreaY for a quick plot: ```svelte live - Math.cos(v / 5))} - opacity={0.5} - canvas={$useCanvas} /> - + testid="area-y-numbers" + grid + height={200} + y={{ ticks: [-1, 0, 1] }}> + Math.cos(v / 5))} + opacity={0.5} + canvas={$useCanvas} /> + ``` ```svelte - Math.cos(v / 5))} - opacity={0.5} /> - + Math.cos(v / 5))} + opacity={0.5} /> + ``` @@ -207,32 +197,32 @@ To create a stacked area chart you can use the implicit [stackY](/transforms/sta ```svelte live - + ``` ```svelte - + ``` @@ -246,38 +236,38 @@ You can control the stacking for the implicit [stackY](/transforms/stack) transf ```svelte live + label="order" + options={['none', 'appearance', 'inside-out', 'sum']} + bind:value={order} /> - + ``` @@ -287,54 +277,54 @@ You can use the **offset** option to create a streamgraph: ```svelte live + label="order" + bind:value={order} + options={['none', 'inside-out', 'appearance', 'sum']} /> - + grid + marginLeft={0} + x={{ grid: true }} + y={{ axis: false }} + color={{ legend: true }} + testid="area-y1"> + ``` ```svelte - + ``` @@ -348,19 +338,19 @@ For "vertical" area charts you can use the **AreaX** mark as shorthand ```svelte live - + ``` @@ -379,44 +369,44 @@ Required channels for horizontal area charts: ```svelte live - - - + + + ``` ```svelte - - - + + + ``` @@ -445,69 +435,59 @@ The example below demonstrates how to use the Area mark to create a custom area ```svelte live - - baseline.find((b) => +b.date === +d.Date) - ?.value || 0} - y2="Close" - opacity={0.25} - fill="steelblue" /> - - + + baseline.find((b) => +b.date === +d.Date)?.value || 0} + y2="Close" + opacity={0.25} + fill="steelblue" /> + + ``` ```svelte - - baseline.find((b) => +b.date === +d.Date) - ?.value || 0} - y2="Close" - opacity={0.25} - fill="steelblue" /> - - + + baseline.find((b) => +b.date === +d.Date)?.value || 0} + y2="Close" + opacity={0.25} + fill="steelblue" /> + + ``` diff --git a/src/routes/marks/arrow/+page.md b/src/routes/marks/arrow/+page.md index 787f2f119..eba2a2f99 100644 --- a/src/routes/marks/arrow/+page.md +++ b/src/routes/marks/arrow/+page.md @@ -1,5 +1,12 @@ --- title: Arrow mark +description: Arrow mark draws a directed arrow between two positions in data space. +examples: + - arrow/metro + - arrow/metro-canvas +links: + examples: /examples/arrow + api: /api/marks#Arrow --- [API Reference](/api/marks#Arrow) @@ -17,93 +24,91 @@ Metro dataset: ```svelte live - - !hl || hl.Metro === d.Metro ? 1 : 0.1 - }} - onmouseenter={(evt, d) => (hl = d)} - onmouseleave={() => (hl = null)} - stroke={(d) => d.R90_10_2015 - d.R90_10_1980} /> - - hl ? hl.Metro === d.Metro : d.highlight} - text="nyt_display" - fill="currentColor" - stroke="var(--svelteplot-bg)" - strokeWidth={4} - lineAnchor="bottom" - dy={-6} /> + grid + marginRight={20} + inset={10} + height={450} + x={{ type: 'log', label: 'P..opulation' }} + y={{ label: 'Inequality' }} + color={{ + label: 'Change in inequality from 1980 to 2015', + legend: true, + tickFormat: '+f' + }}> + (!hl || hl.Metro === d.Metro ? 1 : 0.1) + }} + onmouseenter={(evt, d) => (hl = d)} + onmouseleave={() => (hl = null)} + stroke={(d) => d.R90_10_2015 - d.R90_10_1980} /> + + hl ? hl.Metro === d.Metro : d.highlight} + text="nyt_display" + fill="currentColor" + stroke="var(--svelteplot-bg)" + strokeWidth={4} + lineAnchor="bottom" + dy={-6} /> ``` ```svelte - d.R90_10_2015 - d.R90_10_1980} - bend - opacity={{ - scale: null, - value: (d) => - !hl || hl.Metro === d.Metro ? 1 : 0.1 - }} - onmouseenter={(evt, d) => (hl = d)} - onmouseleave={() => (hl = null)} /> - - hl ? hl.Metro === d.Metro : d.highlight} - text="nyt_display" - fill="currentColor" - stroke="var(--svelteplot-bg)" - strokeWidth={4} - lineAnchor="bottom" - dy={-6} /> + x={{ type: 'log', label: 'Population' }} + y={{ label: 'Inequality', type: 'point' }} + color={{ + scheme: 'BuRd', + label: 'Change in inequality from 1980 to 2015', + legend: true, + tickFormat: '+f' + }}> + d.R90_10_2015 - d.R90_10_1980} + bend + opacity={{ + scale: null, + value: (d) => (!hl || hl.Metro === d.Metro ? 1 : 0.1) + }} + onmouseenter={(evt, d) => (hl = d)} + onmouseleave={() => (hl = null)} /> + + hl ? hl.Metro === d.Metro : d.highlight} + text="nyt_display" + fill="currentColor" + stroke="var(--svelteplot-bg)" + strokeWidth={4} + lineAnchor="bottom" + dy={-6} /> ``` @@ -113,44 +118,44 @@ Works as well with a point scale: ```svelte live - - - + inset={10} + x={{ label: 'Population' }} + y={{ label: '' }}> + + + ``` ```svelte - - - + inset={10} + x={{ label: 'Population' }} + y={{ label: '' }}> + + + ``` @@ -160,98 +165,98 @@ Another thing you can use the arrow mark for is drawing network diagrams: ```svelte live - d.source.x} - y1={(d) => d.source.y} - x2={(d) => d.target.x} - y2={(d) => d.target.y} - bend - insetStart={(d) => d.source.id.length * 1} - insetEnd={(d) => d.target.id.length * 1} - opacity={0.2} /> - d.id.length} + x={{ axis: false }} + y={{ axis: false }} + inset={10} + color={{ type: 'categorical' }} + height={550}> + d.source.x} + y1={(d) => d.source.y} + x2={(d) => d.target.x} + y2={(d) => d.target.y} + bend + insetStart={(d) => d.source.id.length * 1} + insetEnd={(d) => d.target.id.length * 1} + opacity={0.2} /> + d.id.length} + stroke="var(--svelteplot-bg)" + fill="group" + x="x" + y="y" /> + + {#snippet children({ data })} + - - {#snippet children({ data })} - - {/snippet} - + {/snippet} + ``` ```svelte - d.source.x} - y1={(d) => d.source.y} - x2={(d) => d.target.x} - y2={(d) => d.target.y} - bend - insetStart={(d) => d.source.id.length * 1} - insetEnd={(d) => d.target.id.length * 1} /> - d.id.length} - fill="group" - x="x" - y="y" /> + d.source.x} + y1={(d) => d.source.y} + x2={(d) => d.target.x} + y2={(d) => d.target.y} + bend + insetStart={(d) => d.source.id.length * 1} + insetEnd={(d) => d.target.id.length * 1} /> + d.id.length} + fill="group" + x="x" + y="y" /> ``` @@ -270,19 +275,19 @@ The Arrow mark offers various customization options to control appearance and be **Arrow Style and Shape** - **bend** - Controls the curvature of the arrow in degrees: - - Set to a number (e.g., `30`) for a specific bend angle - - Set to `true` for a default bend of 22.5° - - Set to `0` or `false` for a straight arrow - - Positive values curve clockwise, negative values curve counterclockwise + - Set to a number (e.g., `30`) for a specific bend angle + - Set to `true` for a default bend of 22.5° + - Set to `0` or `false` for a straight arrow + - Positive values curve clockwise, negative values curve counterclockwise - **sweep** - Controls the direction of the bend: - - `1` (default) - Standard direction - - `0` - No bending (straight line regardless of bend angle) - - `-1` - Reverse direction - - `'+x'` - Bend follows the x-axis in positive direction - - `'-x'` - Bend follows the x-axis in negative direction - - `'+y'` - Bend follows the y-axis in positive direction - - `'-y'` - Bend follows the y-axis in negative direction + - `1` (default) - Standard direction + - `0` - No bending (straight line regardless of bend angle) + - `-1` - Reverse direction + - `'+x'` - Bend follows the x-axis in positive direction + - `'-x'` - Bend follows the x-axis in negative direction + - `'+y'` - Bend follows the y-axis in positive direction + - `'-y'` - Bend follows the y-axis in negative direction - **headAngle** - The arrowhead angle in degrees (default: 60°) - **headLength** - Controls the size of the arrowhead (default: 8) @@ -310,62 +315,50 @@ Event handlers can be attached to arrows for interactive visualizations: ```svelte live - {#snippet header()} -
    - - - - - +
    + {/snippet} + + +
    ``` diff --git a/src/routes/marks/axis/+page.md b/src/routes/marks/axis/+page.md index f1590b95d..e251fd58d 100644 --- a/src/routes/marks/axis/+page.md +++ b/src/routes/marks/axis/+page.md @@ -1,25 +1,35 @@ --- title: Axis mark +description: AxisX and AxisY render tick marks and labels along a scale; SveltePlot adds them implicitly unless you provide your own. +examples: + - axis/rotated-labels + - axis/major-minor + - axis/tick-intervals + - axis/datawrapper-ticks + - axis/wordwrap +links: + examples: /examples/axis + api: /api/marks#AxisX --- Axis marks are useful for rendering the x and y axes! Since they are useful in 95% of plots, SveltePlot will create axis marks by default (implicit axes): ```svelte live - + ``` ```svelte - + ``` @@ -29,20 +39,20 @@ You can turn the implicit axes off by adding `axis: false` ```svelte live - + ``` ```svelte - + ``` @@ -52,19 +62,19 @@ You can also control the implicit axes individually using the x and y options. H ```svelte live - + ``` ```svelte - + ``` @@ -75,15 +85,15 @@ You can also control the implicit axes individually using the x and y options. H When using implicit axes (the default), you can customize them via the `x` and `y` scale options: - `axis` - controls which sides of the plot have axes - - For x-axis: 'top', 'bottom', 'both', or false - - For y-axis: 'left', 'right', 'both', or false + - For x-axis: 'top', 'bottom', 'both', or false + - For y-axis: 'left', 'right', 'both', or false - `tickSpacing` - spacing between ticks in pixels - `tickFormat` - format for tick labels (can be 'auto', format string, or custom function) - `tickRotate` - rotation angle for tick labels in degrees - `label` - the axis label - `labelAnchor` - position of axis labels - - For x-axis: 'auto', 'left', 'center', 'right' - - For y-axis: 'auto', 'bottom', 'middle', 'top' + - For x-axis: 'auto', 'left', 'center', 'right' + - For y-axis: 'auto', 'bottom', 'middle', 'top' - `insetLeft/Right` - insets for x-axis positioning - `insetTop/Bottom` - insets for y-axis positioning @@ -91,40 +101,40 @@ A combination of options: ```svelte live d.toFixed(2) - }}> - + x={{ + axis: 'both' + }} + y={{ + axis: 'right', + label: '', + ticks: 25, + tickFormat: (d) => d.toFixed(2) + }}> + ``` ```svelte `$${d}`, - tickRotate: -45 - }} - y={{ - axis: 'both', - ticks: 25, - tickFormat: (d) => d.toFixed(2) - }}> - + x={{ + axis: 'bottom', + tickSpacing: 100, + tickFormat: (d) => `$${d}`, + tickRotate: -45 + }} + y={{ + axis: 'both', + ticks: 25, + tickFormat: (d) => d.toFixed(2) + }}> + ``` @@ -134,45 +144,45 @@ If you add the `AxisX` and `AxisY` marks to your plot, SveltePlot will disable t ```svelte live - - - + + + ``` ```svelte - - - + + + ``` ```svelte - - - - + frame + x={{ domain: [0, 10] }} + y={{ domain: [0, 5] }} + marginBottom={40} + marginRight={30}> + + + + ``` @@ -180,10 +190,10 @@ The automatic ticks can be customized using the tickSpacing option: ```svelte - + x={{ tickSpacing: 150 }} + y={{ tickSpacing: 10 }} + testid="tickspacing"> + ``` @@ -191,68 +201,62 @@ Ordinal axis: ```svelte live + x={{ + domain: + 'These are some ordinal ticks on a band scale'.split( + ' ' + ) + }} /> ``` ```svelte + x={{ + domain: + 'These are some ordinal ticks on a band scale'.split( + ' ' + ) + }} /> ``` You can change the defaults for SveltePlot grids using the `setPlotDefaults` hook: ```svelte live - + ``` ```svelte - + ``` @@ -319,17 +323,17 @@ You can customize the automatic axis ticks using the `interval` and `tickSpacing ```svelte live + bind:value={interval} + min={5} + max={50} + label="Interval" /> ``` @@ -343,58 +347,52 @@ For time-scales you can define the interval as string: ```svelte live + label="interpolate" + options={INTERPOLATIONS} + bind:value={interpolate} /> - - - - - - - - + projection={{ + type: 'conic-conformal', + domain: land, + rotate: [96, 0], + center: [0, 56], + parallels: [49, 77] + }} + color={{ + scheme: 'burd', + type: 'diverging', + legend: true + }}> + + + + + + + + ``` @@ -251,29 +253,29 @@ Omit `data` and pass `value` as an `(x, y) => number` function with explicit dom ```svelte - Math.sin(x) * Math.cos(y)} - x1={0} - x2={6 * Math.PI} - y1={0} - y2={4 * Math.PI} - fill="value" /> + Math.sin(x) * Math.cos(y)} + x1={0} + x2={6 * Math.PI} + y1={0} + y2={4 * Math.PI} + fill="value" /> ``` ```svelte live - Math.sin(x) * Math.cos(y)} - x1={0} - x2={6 * Math.PI} - y1={0} - y2={4 * Math.PI} - fill="value" /> + Math.sin(x) * Math.cos(y)} + x1={0} + x2={6 * Math.PI} + y1={0} + y2={4 * Math.PI} + fill="value" /> ``` @@ -285,55 +287,55 @@ Omit `data` and pass `value` as an `(x, y) => number` function with explicit dom ```svelte + bind:value={blur} + label="blur" + min={0} + max={6} + step={0.1} /> - + ``` ```svelte live + bind:value={blur} + label="blur" + min={0} + max={6} + step={0.1} /> - + ``` @@ -343,25 +345,25 @@ Use `thresholds` to set the approximate number of levels, an explicit array, or ```svelte live + label="thresholds" + bind:value={thresholds} + min={3} + max={40} /> - + ``` @@ -373,25 +375,25 @@ Use `interval` for evenly-spaced steps: ```svelte live + label="interval" + bind:value={interval} + min={3} + max={40} /> - + ``` @@ -403,21 +405,21 @@ You can also pass an explicit array of threshold values: ```svelte live - + ``` @@ -427,88 +429,82 @@ With evenly-spaced thresholds, contour bands cover equal data ranges but unequal ```svelte live - - - - - - - - + projection={{ + type: 'conic-conformal', + domain: land, + rotate: [96, 0], + center: [0, 56], + parallels: [49, 77] + }} + color={{ + scheme: 'burd', + type: 'threshold', + domain: colorDomain, + legend: true + }}> + + + + + + + + ``` diff --git a/src/routes/marks/custom/+page.md b/src/routes/marks/custom/+page.md index 2a51958aa..9e2d45965 100644 --- a/src/routes/marks/custom/+page.md +++ b/src/routes/marks/custom/+page.md @@ -1,51 +1,55 @@ --- title: Custom marks +description: CustomMark and CustomMarkHTML embed arbitrary SVG or HTML elements at data-space positions — for when built-in marks are not enough. +examples: + - custom/single + - custom/multiple + - custom/histogram-topline +links: + examples: /examples/custom + api: /api/marks#CustomMark --- You can use the custom mark to render your own marks. You can pass data to the custom mark and use the plot scales. Let's say we want to render our own symbols instead of using the [dot mark](/marks/dot): ```svelte live - - {#snippet mark({ record })} - - - - {/snippet} - + + {#snippet mark({ record })} + + + + {/snippet} + ``` ```svelte - {#snippet mark({ record })} - - - - {/snippet} + data={penguins} + x="bill_length_mm" + y="bill_depth_mm" + stroke="species"> + {#snippet mark({ record })} + + + + {/snippet} ``` @@ -53,40 +57,36 @@ We can also pass the `marks` (plural) snippet to draw all symbols at once: ```svelte - {#snippet marks({ records })} - [r.x, r.y]) - .join(' ')} /> - {/snippet} + data={penguins} + x="bill_length_mm" + y="bill_depth_mm"> + {#snippet marks({ records })} + [r.x, r.y]).join(' ')} /> + {/snippet} ``` ```svelte live - - {#snippet marks({ records })} - [r.x, r.y]) - .join(' ')} /> - {/snippet} - + + {#snippet marks({ records })} + [r.x, r.y]).join(' ')} /> + {/snippet} + ``` @@ -96,55 +96,55 @@ Or we can use a custom mark to draw the topline of a histogram: ```svelte live - {@const binned = binX( - { data: olympians, x: 'weight' }, - { y: 'count', interval: 3 } - )} - - - {#snippet marks({ records })} - - {/snippet} - + {@const binned = binX( + { data: olympians, x: 'weight' }, + { y: 'count', interval: 3 } + )} + + + {#snippet marks({ records })} + + {/snippet} + ``` ```svelte - {@const binned = binX( - { data: olympians, x: 'weight' }, - { y: 'count', interval: 3 } - )} - - - {#snippet marks({ records })} - - {/snippet} - + {@const binned = binX( + { data: olympians, x: 'weight' }, + { y: 'count', interval: 3 } + )} + + + {#snippet marks({ records })} + + {/snippet} + ``` @@ -154,9 +154,9 @@ Or we can use a custom mark to draw the topline of a histogram: ```svelte - {#snippet mark({ record })} - - {/snippet} + {#snippet mark({ record })} + + {/snippet} ``` @@ -168,109 +168,109 @@ You can arrange custom HTML elements in the plot using the `CustomMarkHTML` mark ```svelte live - - {#snippet overlay()} - - {#snippet children({ datum })} -
    - {datum.species} -
    - {/snippet} -
    - {/snippet} + + {#snippet overlay()} + + {#snippet children({ datum })} +
    + {datum.species} +
    + {/snippet} +
    + {/snippet}
    ``` ```svelte - - {#snippet overlay()} - - {#snippet children({ datum })} -
    - {datum.species} -
    - {/snippet} -
    - {/snippet} + + {#snippet overlay()} + + {#snippet children({ datum })} +
    + {datum.species} +
    + {/snippet} +
    + {/snippet}
    ``` ```svelte live - - {#snippet overlay()} - - {#snippet children({ datum })} -
    - {datum.i}: {datum.species} - {datum.dy} -
    - {/snippet} -
    - {/snippet} + + {#snippet overlay()} + + {#snippet children({ datum })} +
    + {datum.i}: {datum.species} + {datum.dy} +
    + {/snippet} +
    + {/snippet}
    ``` diff --git a/src/routes/marks/delaunay/+page.md b/src/routes/marks/delaunay/+page.md index c704f4e76..b65dad01d 100644 --- a/src/routes/marks/delaunay/+page.md +++ b/src/routes/marks/delaunay/+page.md @@ -1,11 +1,14 @@ --- title: Delaunay / Voronoi marks +description: Delaunay and Voronoi marks tessellate a point cloud into triangles or cells — for proximity diagrams, hull outlines, and nearest-neighbour overlays. +examples: + - delaunay/voronoi-mesh-walmart +links: + examples: /examples/delaunay + api: /api/marks#DelaunayLink +addedIn: '0.14.0' --- -:::info -added in 0.14.0 -::: - The Delaunay marks compute a [Delaunay triangulation](https://en.wikipedia.org/wiki/Delaunay_triangulation) or its dual [Voronoi diagram](https://en.wikipedia.org/wiki/Voronoi_diagram) from **x** and **y** positions. Five marks are available: | Mark | Description | @@ -22,46 +25,46 @@ The **Voronoi** mark partitions the plane into cells, one per data point, each c ```svelte live - - - + + + ``` ```svelte - - - + + + ``` @@ -73,40 +76,40 @@ The **VoronoiMesh** mark renders the full Voronoi diagram as a single ``, ```svelte live - - + + ``` ```svelte - - + + ``` @@ -114,53 +117,43 @@ The Delaunay marks work great with the [projection](/marks/geo) system. Here is ```svelte live - - - + projection="albers-usa" + height={420} + testid="voronoi-walmart"> + + + ``` ```svelte - - - + + + ``` @@ -172,42 +165,42 @@ The **DelaunayMesh** mark renders the full Delaunay triangulation as a single `< ```svelte live - - + + ``` ```svelte - - + + ``` @@ -217,44 +210,44 @@ You can use the `z` channel to group the meshes: ```svelte live - - + + ``` ```svelte - - + + ``` @@ -266,42 +259,42 @@ The **DelaunayLink** mark renders individual Delaunay edges as separate paths, a ```svelte live - - + + ``` ```svelte - - + + ``` @@ -313,46 +306,46 @@ The **Hull** mark renders the convex hull of data points. Use the **z**, **fill* ```svelte live - - + + ``` ```svelte - - + + ``` diff --git a/src/routes/marks/density/+page.md b/src/routes/marks/density/+page.md index d586beced..9bb1d64c1 100644 --- a/src/routes/marks/density/+page.md +++ b/src/routes/marks/density/+page.md @@ -1,11 +1,16 @@ --- title: Density mark +description: The Density mark estimates a kernel density and draws the result as isolines, filled bands, or a smoothed area. +examples: + - density/basic + - density/colored-isolines + - density/grouped +links: + examples: /examples/density + api: /api/marks#Density +addedIn: '0.14.0' --- -:::info -added in 0.14.0 -::: - The **density mark** estimates and renders a [two-dimensional kernel density](https://en.wikipedia.org/wiki/Multivariate_kernel_density_estimation) from scatter data. It uses a Gaussian kernel applied to data projected into pixel space, then draws [iso-density contours](https://en.wikipedia.org/wiki/Contour_line) using the marching-squares algorithm. :::tip @@ -14,58 +19,56 @@ For one-dimensional kernel density estimates, see the [densityX](/transforms/den ```svelte live - - + + ``` -[API Reference](/api/marks#Density) - Pass `data` with `x` and `y` channels. The mark computes density across the plot area and draws contour lines at automatically-chosen levels: ```svelte - + ``` ```svelte live - + ``` @@ -75,28 +78,28 @@ You can create separate densities by grouping via stroke or `z` channel: ```svelte live - + ``` ```svelte - + ``` @@ -104,38 +107,38 @@ Set `fill="density"` to fill each contour band by its estimated density using th ```svelte - + ``` ```svelte live - + color={{ + scheme: ds.isDark ? 'viridis' : 'blues', + legend: true + }}> + ``` @@ -145,19 +148,19 @@ Use `stroke="density"` to color each isoline by its density level: ```svelte live - + ``` @@ -169,29 +172,29 @@ The `bandwidth` option (default 20) controls the Gaussian kernel's standard devi ```svelte live + label="bandwidth" + bind:value={bandwidth} + min={5} + max={60} /> - + ``` @@ -204,26 +207,26 @@ Control the number and placement of density levels with `thresholds`: ```svelte live + label="thresholds" + bind:value={thresholds} + min={2} + max={30} /> - + ``` @@ -233,40 +236,40 @@ The density mark supports faceting via `fx` and `fy`. Each facet panel computes ```svelte - + ``` ```svelte live - - + + ``` @@ -278,38 +281,38 @@ Use `weight` to give different data points different contributions to the densit ```svelte live + bind:value={skew} + min={-1} + max={1} + step={0.01} + label="Skew (-F/+M)" /> - - d.sex === 'FEMALE' ? 1 - skew : 1 + skew} - thresholds={10} /> - + + d.sex === 'FEMALE' ? 1 - skew : 1 + skew} + thresholds={10} /> + ``` diff --git a/src/routes/marks/difference/+page.md b/src/routes/marks/difference/+page.md index d6fe1ecae..267e89e2f 100644 --- a/src/routes/marks/difference/+page.md +++ b/src/routes/marks/difference/+page.md @@ -1,5 +1,13 @@ --- title: Difference mark +description: DifferenceY shades the area between two lines in contrasting colors above and below — for highlighting when one series exceeds the other. +examples: + - difference/anomaly-baseline + - difference/trade-balance + - difference/apple-yoy +links: + examples: /examples/difference + api: /api/marks#DifferenceY --- The **difference mark** can be used to fill the areas between a _metric_ line and a _comparison_ line/value colored based whether or not the difference is positive or negative. @@ -8,59 +16,59 @@ The following example shows trade between the USA and the UK, with the exports f ```svelte live - - - + + + ``` ```svelte - - - + + + ``` @@ -68,38 +76,38 @@ If just one _x_ and _y_ channel is defined, the value zero will be used as compa ```svelte live - - + + ``` ```svelte - - + + ``` @@ -107,48 +115,48 @@ You can compare the metric to a different "baseline" by providing a constant _y1 ```svelte live + label="y1" + min={-0.4} + max={1} + step={0.01} + bind:value={y1} /> - - + + ``` ```svelte - - + + ``` @@ -156,43 +164,43 @@ In combination with the [shift transform](/transforms/shift) you can compare a s ```svelte live - + ``` ```svelte - + ``` diff --git a/src/routes/marks/dot/+page.md b/src/routes/marks/dot/+page.md index b868eadd7..0c87a139a 100644 --- a/src/routes/marks/dot/+page.md +++ b/src/routes/marks/dot/+page.md @@ -1,46 +1,55 @@ --- title: Dot mark +description: The dot mark draws circles or other symbols positioned in x and y as in a scatterplot. +examples: + - dot/1-colored-scatterplot + - dot/2-symbol-channel + - dot/3-dot-plot + - dot/beeswarm-bubbles + - dot/jitter +links: + examples: /examples/dot + api: /api/marks#Dot --- -The dot mark draws circles or other symbols positioned in **x** and **y** as in a scatterplot. For example, the chart below shows the roughly-inverse relationship between car horsepower in y↑ and fuel efficiency in miles per gallon in x→. ```svelte live
    + label="max cylinders" + bind:value={maxCylinders} + min={1} + max={10} /> - d.cylinders <= maxCylinders} - data={cars} - x="economy (mpg)" - y="power (hp)" - {...{ [fill ? 'fill' : 'stroke']: 'manufactor' }} - symbol="manufactor" /> + d.cylinders <= maxCylinders} + data={cars} + x="economy (mpg)" + y="power (hp)" + {...{ [fill ? 'fill' : 'stroke']: 'manufactor' }} + symbol="manufactor" /> ``` @@ -48,28 +57,25 @@ When showing plots with a lot of dots, you can switch to canvas rendering to imp ```svelte live - [ - randX(), - randY() - ])} /> + [randX(), randY()])} /> ``` @@ -77,18 +83,18 @@ This example uses stroke color and mark shape/symbol to redundantly encode a cat ```svelte live - + ``` @@ -96,30 +102,27 @@ One more ```svelte live max radius:
    + type="range" + bind:value={maxRad} + min={0} + max={20} />
    - - Math.pow( - d.culmen_length_mm * d.culmen_depth_mm, - 4 - )} - fill="sex" /> + + Math.pow(d.bill_length_mm * d.bill_depth_mm, 4)} + fill="sex" /> ``` @@ -127,31 +130,71 @@ You can also use a point scale for dot dimensions to create dot plots, such as t ```svelte live - - d['Total speakers'] >= 70e6 - )} - fill="currentColor" - sort={{ channel: '-x' }} - y="Language" - x="Total speakers" /> + frame + inset={20} + testid="languages" + x={{ + type: 'log', + axis: 'both', + label: 'NUMBER OF SPEAKERS', + labelAnchor: 'center' + }} + y={{ type: 'point', label: '' }}> + + d['Total speakers'] >= 70e6 + )} + fill="currentColor" + sort={{ channel: '-x' }} + y="Language" + x="Total speakers" /> + +``` + +## Symbol types + +The `symbol` channel accepts a string name to pick from the built-in set: + +```svelte live + + + + strokeOnly.has(d.name) ? null : 'currentColor'} + stroke={(d) => strokeOnly.has(d.name) ? 'currentColor' : null} /> + ``` @@ -163,13 +206,13 @@ Using the **DotX** mark, you can quickly plot a list of numbers as dots: ```svelte live - d['economy (mpg)'])} /> + d['economy (mpg)'])} /> ``` @@ -181,13 +224,13 @@ Using the DotY mark, you can quickly plot a list of numbers as dots: ```svelte live - d['economy (mpg)'])} /> + d['economy (mpg)'])} /> ``` @@ -197,32 +240,32 @@ You can use the color channel for encoding a third quantitative variable. ```svelte live - - {#if !decline} - - {/if} + grid + height={400} + color={{ + legend: false, + type: 'linear', + domain: decline ? null : [1, 10], + scheme: 'rdylgn', + label: 'IMDB rating' + }}> + + {#if !decline} + + {/if} ``` diff --git a/src/routes/marks/frame/+page.md b/src/routes/marks/frame/+page.md index 8cf11a519..a7cdc3b7e 100644 --- a/src/routes/marks/frame/+page.md +++ b/src/routes/marks/frame/+page.md @@ -1,5 +1,13 @@ --- title: Frame mark +description: Frame mark draws a border rectangle around the plot area — useful for chart types where a visible boundary aids readability. +examples: + - frame/explicit-frame + - frame/implicit-frame + - frame/frame-dx-dy +links: + examples: /examples/frame + api: /api/marks#Frame --- [API Reference](/api/marks#Frame) @@ -9,19 +17,19 @@ easiest way to add a frame is to set the frame option of the Plot element ```svelte live - + ``` ```svelte - + ``` @@ -31,33 +39,33 @@ If you need more customization options, you can add the frame manually by explic ```svelte live - - - + + + ``` ```svelte - - - + + + ``` @@ -65,30 +73,30 @@ You can use the explicit frame and grid marks to create ggplot style charts: ```svelte live - - - - + + + + ``` ```svelte - - - - + + + + ``` diff --git a/src/routes/marks/geo/+page.md b/src/routes/marks/geo/+page.md index 4cf5e5f94..03c0c629b 100644 --- a/src/routes/marks/geo/+page.md +++ b/src/routes/marks/geo/+page.md @@ -1,5 +1,14 @@ --- title: Geo mark +description: Geo mark renders GeoJSON geometries using a cartographic projection — the foundation for choropleth maps and geographic visualisations. +examples: + - geo/us-choropleth + - geo/earthquakes + - geo/custom-proj + - geo/inset-aspect +links: + examples: /examples/geo + api: /api/marks#Geo --- {#if curve.includes('bundle') || curve.includes('catmull') || curve.includes('cardinal')} - + {/if} - - - ({ - value: d, - index: i - }))} - symbol="plus" - y="value" - x="index" /> + + + ({ + value: d, + index: i + }))} + symbol="plus" + y="value" + x="index" /> ``` ```svelte - + ``` @@ -594,39 +584,39 @@ Lines can show a text label along the path: ```svelte live + bind:value={offset} + label="textStartOffset" + min={0} + max={100} /> - + ``` ```svelte - + ``` @@ -638,44 +628,44 @@ Line charts do not support implicit stacking, but you can use the [stack](/trans ```svelte live - - {#each ['y1', 'y2'] as y} - - {/each} + + {#each ['y1', 'y2'] as y} + + {/each} ``` ```svelte - - {#each ['y1', 'y2'] as y} - - {/each} + + {#each ['y1', 'y2'] as y} + + {/each} ``` @@ -689,95 +679,92 @@ Like all marks, Line marks support [faceting](/features/faceting). In this examp ```svelte live d.getFullYear() - }} - y={{ axis: false }}> - - - /> + height={innerWidth.current < 500 ? 400 : 130} + inset={5} + x={{ + interval: 'year', + grid: true, + tickFormat: (d) => d.getFullYear() + }} + y={{ axis: false }}> + + + /> ``` ```svelte + {...normalizeY( + { + data: stocks, + x: 'Date', + y: 'Close', + stroke: 'Symbol' + }, + 'extent' + )} + curve="basis" + fx="Symbol" /> /> ``` @@ -787,49 +774,49 @@ You can use the [LinearGradient](/features/gradients) helper components to color ```svelte live - - - - + + + + ``` ```svelte - - - - + + + + ``` diff --git a/src/routes/marks/link/+page.md b/src/routes/marks/link/+page.md index d65502448..375ec854c 100644 --- a/src/routes/marks/link/+page.md +++ b/src/routes/marks/link/+page.md @@ -1,5 +1,12 @@ --- title: Link mark +description: Link mark draws a straight line segment between two explicit (x1, y1) → (x2, y2) positions in data space. +examples: + - link/metros + - link/spherical-link +links: + examples: /examples/link + api: /api/marks#Link --- [API Reference](/api/marks#Link) @@ -8,55 +15,54 @@ For connecting two points with a line. ```svelte live - - !hl || hl.Metro === d.Metro ? 1 : 0.1 - }} - onmouseenter={(event, d) => (hl = d)} - onmouseleave={() => (hl = null)} - stroke={(d) => d.R90_10_2015 - d.R90_10_1980} /> - - hl ? d.Metro === hl.Metro : d.highlight} - text="nyt_display" - fill="currentColor" - stroke="var(--svelteplot-bg)" - strokeWidth={4} - lineAnchor="bottom" - dy={-6} /> + grid + marginRight={20} + inset={10} + height={450} + x={{ type: 'log', label: 'Population' }} + y={{ label: 'Inequality' }} + color={{ + scheme: 'BuRd', + pivot: 1.5, + label: 'Change in inequality from 1980 to 2015', + legend: true + }}> + (!hl || hl.Metro === d.Metro ? 1 : 0.1) + }} + onmouseenter={(event, d) => (hl = d)} + onmouseleave={() => (hl = null)} + stroke={(d) => d.R90_10_2015 - d.R90_10_1980} /> + + hl ? d.Metro === hl.Metro : d.highlight} + text="nyt_display" + fill="currentColor" + stroke="var(--svelteplot-bg)" + strokeWidth={4} + lineAnchor="bottom" + dy={-6} /> ``` @@ -64,52 +70,52 @@ Link support spherical projections: ```svelte live - - - - + + + + ``` diff --git a/src/routes/marks/pointer/+page.md b/src/routes/marks/pointer/+page.md index 8154610fd..289e005da 100644 --- a/src/routes/marks/pointer/+page.md +++ b/src/routes/marks/pointer/+page.md @@ -1,68 +1,74 @@ --- title: Pointer mark +description: Pointer mark highlights the data point nearest to the cursor, enabling hover tooltips and interactive cross-filtering. +examples: + - pointer/crosshair + - pointer/grouped + - pointer/scanline-y + - pointer/faceted-1 + - pointer/faceted-2 +links: + examples: /examples/pointer + api: /api/marks#Pointer --- Pointer is a mark that doesn't render anything by itself, but you can use it to show marks filtered to data points close to the cursor. You access the filtered data by placing the marks as children of the Pointer mark: ```svelte live - - + + {#snippet children({ data })} + - {#snippet children({ data })} - d.Close.toFixed()} - lineAnchor="bottom" - fontWeight="bold" - dy={-5} /> - - {/snippet} - + text={(d) => d.Close.toFixed()} + lineAnchor="bottom" + fontWeight="bold" + dy={-5} /> + + {/snippet} + ``` ```svelte - - - {#snippet children({ data })} - d.Close.toFixed()} - lineAnchor="bottom" - fontWeight="bold" - dy={-5} /> - - {/snippet} - + + + {#snippet children({ data })} + d.Close.toFixed()} + lineAnchor="bottom" + fontWeight="bold" + dy={-5} /> + + {/snippet} + ``` @@ -72,67 +78,63 @@ You can create a "crosshair" mark by wrapping grids and axes marks inside a poin ```svelte live
    - - - - - - {#snippet children({ data })} - {#if data.length > 0} - - - d.Date)} - tickFormat="MMM D, YYYY" /> - d.Close)} - tickFormat={(d) => d.toFixed()} /> - {/if} - {/snippet} - - -
    -``` - -```svelte - + - {#snippet children({ data })} - - - d.Date)} - tickFormat={(d) => d.getFullYear()} /> - d.Close)} - tickFormat={(d) => d.toFixed()} /> - {/snippet} + data={aapl} + x="Date" + y="Close" + maxDistance={30}> + {#snippet children({ data })} + {#if data.length > 0} + + + d.Date)} + tickFormat="MMM D, YYYY" /> + d.Close)} + tickFormat={(d) => d.toFixed()} /> + {/if} + {/snippet} + +
    +``` + +```svelte + + + + + + {#snippet children({ data })} + + + d.Date)} + tickFormat={(d) => d.getFullYear()} /> + d.Close)} + tickFormat={(d) => d.toFixed()} /> + {/snippet} + ``` @@ -140,50 +142,50 @@ If we only pass an **x** channel to the Pointer mark it will try to find the clo ```svelte live - - - {#snippet children({ data })} - - d.Close.toFixed()} - lineAnchor="bottom" - fontWeight="bold" - dy="-10" /> - - {/snippet} - + + + {#snippet children({ data })} + + d.Close.toFixed()} + lineAnchor="bottom" + fontWeight="bold" + dy="-10" /> + + {/snippet} + ``` ```svelte - - - {#snippet children({ data })} - - - - {/snippet} - + + + {#snippet children({ data })} + + + + {/snippet} + ``` @@ -191,68 +193,68 @@ We can use the `onupdate` event to fade out marks whenever points are highlighte ```svelte live - + (sel = e)}> + {#snippet children({ data })} + + - (sel = e)}> - {#snippet children({ data })} - - d.Close.toFixed()} - lineAnchor="bottom" - fontWeight="bold" - dy="-10" /> - - {/snippet} - + text={(d) => d.Close.toFixed()} + lineAnchor="bottom" + fontWeight="bold" + dy="-10" /> + + {/snippet} + ``` ```svelte - 0 ? 0.4 : 1} /> - (seletion = data)}> - {#snippet children({ data })} - - - - {/snippet} - + 0 ? 0.4 : 1} /> + (seletion = data)}> + {#snippet children({ data })} + + + + {/snippet} + ``` @@ -260,47 +262,47 @@ This works for the **y** channel as well, but note that we only highlight one po ```svelte live - 0 ? 0.4 : 1} /> - 0 ? 0.4 : 1} /> + (sel = e)}> + {#snippet children({ data })} + + (sel = e)}> - {#snippet children({ data })} - - d.Close.toFixed()} - lineAnchor="middle" - textAnchor="end" - dx={-5} - frameAnchor="left" - fontWeight="bold" /> - - {/snippet} - + text={(d) => d.Close.toFixed()} + lineAnchor="middle" + textAnchor="end" + dx={-5} + frameAnchor="left" + fontWeight="bold" /> + + {/snippet} + ``` @@ -308,48 +310,48 @@ We can change this behavior by passing a **tolerance** option to the Pointer mar ```svelte live - 0 ? 0.4 : 1} /> + (sel = e)}> + {#snippet children({ data })} + + 0 ? 0.4 : 1} /> - (sel = e)}> - {#snippet children({ data })} - - d.Close.toFixed()} - lineAnchor="middle" - textAnchor="end" - dx={-5} - frameAnchor="left" - fontWeight="bold" /> - - {/snippet} - + text={(d) => d.Close.toFixed()} + lineAnchor="middle" + textAnchor="end" + dx={-5} + frameAnchor="left" + fontWeight="bold" /> + + {/snippet} + ``` @@ -357,59 +359,59 @@ Another common use case is to show one point per group. This is useful when you ```svelte live - + 0 ? 0.4 : 1} + y="Close" + stroke="Symbol" /> + (sel = e)} + maxDistance={30}> + {#snippet children({ data })} + 0 ? 0.4 : 1} y="Close" - stroke="Symbol" /> - d.Close.toFixed()} + lineAnchor="bottom" + fontWeight="bold" + dy="-7" /> + (sel = e)} - maxDistance={30}> - {#snippet children({ data })} - d.Close.toFixed()} - lineAnchor="bottom" - fontWeight="bold" - dy="-7" /> - - {/snippet} - + y="Close" + fill="Symbol" + strokeWidth="0.7" + stroke="var(--svelteplot-bg)" /> + {/snippet} + ``` diff --git a/src/routes/marks/raster/+page.md b/src/routes/marks/raster/+page.md index 1e8694633..ca4304409 100644 --- a/src/routes/marks/raster/+page.md +++ b/src/routes/marks/raster/+page.md @@ -1,11 +1,17 @@ --- title: Raster mark +description: Raster mark renders a dense pixel grid by sampling a function or interpolating scattered data — ideal for continuous scalar fields. +examples: + - raster/sampled + - raster/interpolated + - raster/volcano + - raster/weather +links: + examples: /examples/raster + api: /api/marks#Raster +addedIn: '0.12.0' --- -:::info -added in 0.12.0 -::: - [API Reference](/api/marks#Raster) The **raster mark** renders data as an [SVG image](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/image), mapping values through the color scale onto a pixel grid. It supports three distinct modes depending on how data is supplied: @@ -26,36 +32,36 @@ The volcano example below uses an 87 × 61 elevation grid of [Maungawhau (Mt. Ed ```svelte - + ``` ```svelte live - + ``` @@ -63,32 +69,32 @@ By default, the image is stretched to fill the plot using smooth interpolation. ```svelte live - - String(d)} - x={(d, i) => (i % width) + 0.5} - y={(d, i) => Math.floor(i / width) + 0.5} /> + x={{ domain: [0, width] }} + y={{ domain: [0, height] }} + color={{ scheme: 'turbo' }}> + + String(d)} + x={(d, i) => (i % width) + 0.5} + y={(d, i) => Math.floor(i / width) + 0.5} /> ``` @@ -102,39 +108,34 @@ Sometimes your data does not come in a gridded format, but at irregularly distri ```svelte live - - + projection={{ + type: 'conic-conformal', + domain: land, + rotate: [96, 0], + center: [0, 56], + parallels: [49, 77] + }} + color={{ + scheme: 'burd', + type: 'quantile', + legend: true + }}> + + ``` @@ -151,85 +152,80 @@ Four interpolation strategies are available: ```svelte live
    -
    - - + projection={{ + type: 'conic-conformal', + domain: land, + rotate: [96, 0], + center: [0, 56], + parallels: [49, 77] + }} + color={{ + scheme: 'burd', + type: 'quantile' + }}> + + ``` ```svelte - - - + projection={{ + type: 'conic-conformal', + domain: land + }} + color={{ + scheme: 'burd', + type: 'quantile' + }}> + + + ``` @@ -237,72 +233,64 @@ Clipping the raster to the land geometry removes those values outside the coastl ```svelte live {#if type === 'poly'}{/if} + label="order" + bind:value={order} + min={2} + max={6} />{/if} {#if type.startsWith('loess')} - {/if} + {/if} + bind:value={interval} + options={INTERVALS} + label="Interval: " /> - - - d.__y1 > d.__y2 - ? 'var(--svp-red)' - : 'var(--svp-green)'} /> + + + d.__y1 > d.__y2 + ? 'var(--svp-red)' + : 'var(--svp-green)'} /> ``` ```svelte - - - - (d.__y1 > d.__y2 ? 'red' : 'green')} /> + + + + (d.__y1 > d.__y2 ? 'red' : 'green')} /> ``` @@ -545,30 +545,30 @@ You can also map to _r_ as output channel: ```svelte live {#if olympians} - - - + + + {/if} ``` @@ -580,8 +580,8 @@ For two-dimensional binning. ```js let { data, ...channels } = bin( - { data, ...input }, - { ...options, ...output } + { data, ...input }, + { ...options, ...output } ); ``` @@ -589,28 +589,27 @@ Requires _input_ channels _x_ and _y_. Valid output channels for `bin()` are _fi ```svelte live {#if olympians} - - - + + + {/if} ``` @@ -622,10 +621,10 @@ Requires input channel _x_. Valid output channels for `binX()` are _y_, _y1_, _y ```svelte ``` @@ -633,39 +632,39 @@ Demo with area ```svelte live - - + + ``` @@ -677,29 +676,29 @@ Requires input channel _y_. Valid output channels for `binY()` are _x_, _x1_, _x ```svelte live - - + + ``` ```svelte - - + + ``` diff --git a/src/routes/transforms/density/+page.md b/src/routes/transforms/density/+page.md index 2bfe8d78a..ab216939a 100644 --- a/src/routes/transforms/density/+page.md +++ b/src/routes/transforms/density/+page.md @@ -14,64 +14,64 @@ For two-dimensional densities, see the [density mark](/marks/density). ```svelte live + bind:value={type} + options={['uniform', 'normal']} + label="Distribution type" /> {#if type === 'uniform'} - + {:else} - + {/if} - + ``` @@ -68,17 +68,17 @@ The jitter transform spreads out overlapping points by adding random noise. This The jitter transform accepts the following options: - **type**: Distribution type, either `'uniform'` (default) or `'normal'` - - `uniform`: Evenly distributed points within range [-width, width] - - `normal`: Normal distribution centered at 0 with standard deviation `std` + - `uniform`: Evenly distributed points within range [-width, width] + - `normal`: Normal distribution centered at 0 with standard deviation `std` - **width**: Width of the uniform distribution (default: `0.35`); used when `type` is `'uniform'` - - For numeric data: A number representing the range on either side of the original value - - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) + - For numeric data: A number representing the range on either side of the original value + - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) - **std**: Standard deviation for the normal distribution (default: `0.15`); used when `type` is `'normal'` - - For numeric data: A number representing the standard deviation - - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) + - For numeric data: A number representing the standard deviation + - For date data: A time interval string (e.g., `'1 month'`, `'2 weeks'`, `'3 days'`) - **source**: Optional random number source that produces values in range [0,1) - - Useful for deterministic jittering (testing or reproducibility) - - Can be used with d3's random generators: `randomLcg()` from d3-random + - Useful for deterministic jittering (testing or reproducibility) + - Can be used with d3's random generators: `randomLcg()` from d3-random ## jitterX @@ -88,10 +88,10 @@ Jitters along the x dimension: ```svelte + {...jitterX( + { data: cars, x: 'cylinders' }, + { type: 'normal' } + )} /> ``` ## jitterY @@ -102,10 +102,10 @@ Jitters along the y dimension: ```svelte + {...jitterY( + { data: cars, y: 'cylinders' }, + { type: 'normal' } + )} /> ``` ## jitter @@ -120,17 +120,17 @@ You can use the generic `jitter` transform to jitter along multiple dimensions s ```svelte + {...jitter( + { + data: cars, + x: 'displacement (cc)', + y: 'cylinders' + }, + { + x: { type: 'normal' }, + y: { type: 'uniform', width: 0.5 } + } + )} /> ``` ## Temporal jittering @@ -139,77 +139,76 @@ Jittering also works for temporal data. When jittering Date objects, random time ```svelte live + options={timeIntervals} + bind:value={width} + label={type === 'uniform' ? 'Width' : 'Std'} /> - + ``` ```svelte - + ``` @@ -223,22 +222,22 @@ For reproducible jittering or specialized random distributions, you can provide ```svelte - + ``` diff --git a/src/routes/transforms/map/+page.md b/src/routes/transforms/map/+page.md index 84b4c0747..19ebca0e7 100644 --- a/src/routes/transforms/map/+page.md +++ b/src/routes/transforms/map/+page.md @@ -6,25 +6,25 @@ title: Map transform ```svelte live - + ``` diff --git a/src/routes/transforms/normalize/+page.md b/src/routes/transforms/normalize/+page.md index 32b4e4bc8..d6afdd567 100644 --- a/src/routes/transforms/normalize/+page.md +++ b/src/routes/transforms/normalize/+page.md @@ -6,55 +6,55 @@ Useful for normalizing data series of different measurement units or varying mag ```svelte live + label="reduce" + options={REDUCE_OPTIONS} + bind:value={reduce} /> strict + > strict - - - - + + + + ``` ```svelte - - - - - - - - + + + + + + + + ``` @@ -148,111 +148,111 @@ Note that the window transform is series-aware (it groups by z/fill/stroke befor ```svelte live - - - + + + ``` ```svelte - - - + grid + y={{ type: 'log', base: 5, domain: [50, 1000] }} + marginRight={100}> + + + ``` @@ -260,47 +260,45 @@ Note that _{'{ k: 20 }'}_ doesn't ensure that you're computing a 20-year average ```svelte live - + ``` ```svelte - + ``` @@ -308,52 +306,50 @@ If we just use the `windowY` transform with _{'{ k: 20, strict: 5 }'}_ to comput ```svelte live - --> - - + --> + + ``` @@ -361,41 +357,39 @@ The problem is that especially in pre-modern times, there aren't nearly as many ```svelte live - + grid + y={{ label: 'Measurement count in 20-year interval' }} + height={250}> + ``` ```svelte - + ``` @@ -405,91 +399,89 @@ We can fix this problem by setting the _interval_ option of the window transform ```svelte live + label="strict" + min={1} + max={20} + bind:value={strict} /> - --> - - - + --> + + + ``` ```svelte - - + + ``` diff --git a/src/routes/tutorial/+layout.svelte b/src/routes/tutorial/+layout.svelte new file mode 100644 index 000000000..297dd0895 --- /dev/null +++ b/src/routes/tutorial/+layout.svelte @@ -0,0 +1,124 @@ + + +
    + {@render children()} +
    + + diff --git a/src/routes/tutorial/+page.server.ts b/src/routes/tutorial/+page.server.ts new file mode 100644 index 000000000..bddb84535 --- /dev/null +++ b/src/routes/tutorial/+page.server.ts @@ -0,0 +1,7 @@ +import { redirect } from '@sveltejs/kit'; +import { get_exercise_stubs } from '$lib/server/tutorial.js'; + +export function load() { + const first = get_exercise_stubs()[0]; + redirect(307, `/tutorial/${first.slug}`); +} diff --git a/src/routes/tutorial/[...slug]/+page.server.ts b/src/routes/tutorial/[...slug]/+page.server.ts new file mode 100644 index 000000000..8b069262c --- /dev/null +++ b/src/routes/tutorial/[...slug]/+page.server.ts @@ -0,0 +1,17 @@ +import { error, redirect } from '@sveltejs/kit'; +import { get_exercise_stubs, load_exercise } from '$lib/server/tutorial.js'; +import type { PageServerLoad } from './$types.js'; + +export const load: PageServerLoad = async ({ params }) => { + const exercise = await load_exercise(params.slug); + if (!exercise) { + const first = get_exercise_stubs().find((s) => s.slug.startsWith(params.slug + '/')); + if (first) redirect(307, `/tutorial/${first.slug}`); + error(404, `Tutorial exercise "${params.slug}" not found`); + } + return { exercise, stubs: get_exercise_stubs() }; +}; + +export function entries() { + return get_exercise_stubs().map(({ slug }) => ({ slug })); +} diff --git a/src/routes/tutorial/[...slug]/+page.svelte b/src/routes/tutorial/[...slug]/+page.svelte new file mode 100644 index 000000000..6c56e7316 --- /dev/null +++ b/src/routes/tutorial/[...slug]/+page.svelte @@ -0,0 +1,658 @@ + + + + {data.exercise.title} • SveltePlot Tutorial + + +
    + + {#snippet a()} +
    +
    + + + {#if nav_open} + + + {/if} +
    +
    + + {@html data.exercise.html} +
    + + +
    + {/snippet} + + {#snippet b()} + + {#snippet a()} +
    + {#if workspace.files.filter((f) => f.type === 'file' && !f.basename.startsWith('+')).length > 1} +
    + {#each workspace.files as file (file.name)} + {#if file.type === 'file' && !file.basename.startsWith('+')} + + {/if} + {/each} +
    + {/if} + +
    + {/snippet} + + {#snippet b()} +
    + {#if browser && bundler} + + {/if} +
    + {/snippet} +
    + {/snippet} +
    +
    + + diff --git a/src/routes/why-svelteplot/+page.md b/src/routes/why-svelteplot/+page.md index 04a9a9258..139e545a9 100644 --- a/src/routes/why-svelteplot/+page.md +++ b/src/routes/why-svelteplot/+page.md @@ -13,29 +13,29 @@ This means there is no "scatterplot" component in SveltePlot, but you can use th ```svelte live - + ``` ```svelte - + ``` @@ -45,49 +45,49 @@ You can think of marks as the building blocks for your visualizations -- or the ```svelte live - - d['Total speakers'] >= 90e6 - )} - x="Total speakers" - y="Language" - fill - sort={{ channel: '-x' }} /> + x={{ + type: 'log', + insetLeft: 20, + insetRight: 20, + axis: 'top' + }} + y={{ type: 'point', label: '' }}> + + d['Total speakers'] >= 90e6 + )} + x="Total speakers" + y="Language" + fill + sort={{ channel: '-x' }} /> ``` ```svelte - - - - + x={{ + type: 'log', + insetLeft: 20, + insetRight: 20, + axis: 'top' + }} + y={{ type: 'point', label: '' }}> + + + + ``` @@ -97,38 +97,36 @@ This makes it a lot easier to iterate over different ideas for visualizations. F ```svelte live - - - + x={{ + type: 'log', + axis: 'top', + insetLeft: 20, + insetRight: 20 + }} + y={{ type: 'point', label: '' }}> + + + ``` @@ -153,53 +151,50 @@ And if we wanted to add uncertainty ranges, we can add a [Rule mark](/marks/rule ```svelte live - - - - d['Total speakers'] - d['First-language'] * 0.2} - x2={(d) => - d['Total speakers'] + - d['First-language'] * 0.2} /> - + x={{ + type: 'log', + axis: 'top', + insetLeft: 20, + insetRight: 20 + }} + y={{ type: 'point', label: '' }}> + + + + d['Total speakers'] - d['First-language'] * 0.2} + x2={(d) => + d['Total speakers'] + d['First-language'] * 0.2} /> + ``` @@ -219,28 +214,28 @@ Take the following example, where you can filter the data using the [filter](/tr ```svelte live + bind:value={min} + label="min economy (mpg)" + max={50} /> - d['economy (mpg)'] > min} - y="weight (lb)" - x="power (hp)" - r={4} - stroke="economy (mpg)" /> + d['economy (mpg)'] > min} + y="weight (lb)" + x="power (hp)" + r={4} + stroke="economy (mpg)" /> ``` @@ -248,88 +243,81 @@ Here's an example where we're binding a live-updated dataset to a [Line mark](/m ```svelte live - - {#if rand.length > 1} - - - - d.y.toFixed(0)} /> - {/if} + grid + marginRight={10} + marginLeft={35 + Math.log10(mag) * 5} + inset={10} + y={{ tickSpacing: 30, nice: true }} + x={{ tickSpacing: 90, insetRight: 30 }} + color={{ type: 'categorical' }} + height={250}> + + {#if rand.length > 1} + + + + d.y.toFixed(0)} /> + {/if} ``` @@ -340,60 +328,57 @@ Try clicking on the bars to change their values. ```svelte live - d} - cursor="pointer" - onclick={(e, d, index) => { - data[index] += 0.2 * data[index]; - }} /> - + color={{ scheme: 'reds' }} + x={{ axis: false, padding: data.length < 60 ? 0.1 : 0 }} + y={{ grid: true, domain: domain.current }}> + d} + cursor="pointer" + onclick={(e, d, index) => { + data[index] += 0.2 * data[index]; + }} /> + ``` ```svelte - { - data[index] += 0.2 * data[index]; - }} /> - + color={{ scheme: 'RdBu' }} + y={{ grid: true, domain: domain.current }}> + { + data[index] += 0.2 * data[index]; + }} /> + ``` @@ -405,88 +390,88 @@ You can extend SveltePlot by injecting regular Svelte snippets. For instance, th ```svelte live - - {#snippet marker(id, color)} - - {#if shown} - - {/if} - - {/snippet} - + + {#snippet marker(id, color)} + + {#if shown} + + {/if} + + {/snippet} + ``` ```svelte - - {#snippet marker(id, color)} - - {#if shown} - - {/if} - - {/snippet} - + + {#snippet marker(id, color)} + + {#if shown} + + {/if} + + {/snippet} + ``` diff --git a/src/shared/docs/InlineExamplesGrid.svelte b/src/shared/docs/InlineExamplesGrid.svelte new file mode 100644 index 000000000..bb367422b --- /dev/null +++ b/src/shared/docs/InlineExamplesGrid.svelte @@ -0,0 +1,92 @@ + + +{#if examples.length} +
    + {#each examples as ex (ex.key)} + +
    + {#if allImages[`../../snapshots/${ex.key}.png`]} + + {/if} +
    + +
    + {/each} +
    +{/if} + + diff --git a/src/snapshots/axis/rug-plot.dark.png b/src/snapshots/axis/rug-plot.dark.png new file mode 100644 index 000000000..eef80db8a Binary files /dev/null and b/src/snapshots/axis/rug-plot.dark.png differ diff --git a/src/snapshots/axis/rug-plot.png b/src/snapshots/axis/rug-plot.png new file mode 100644 index 000000000..5be5d26b1 Binary files /dev/null and b/src/snapshots/axis/rug-plot.png differ diff --git a/src/snapshots/delaunay/hull-blur.dark.png b/src/snapshots/delaunay/hull-blur.dark.png new file mode 100644 index 000000000..2bb0eb393 Binary files /dev/null and b/src/snapshots/delaunay/hull-blur.dark.png differ diff --git a/src/snapshots/delaunay/hull-blur.png b/src/snapshots/delaunay/hull-blur.png new file mode 100644 index 000000000..6fd5dd07b Binary files /dev/null and b/src/snapshots/delaunay/hull-blur.png differ diff --git a/src/snapshots/dot/global-facet.dark.png b/src/snapshots/dot/global-facet.dark.png new file mode 100644 index 000000000..60b99b293 Binary files /dev/null and b/src/snapshots/dot/global-facet.dark.png differ diff --git a/src/snapshots/dot/global-facet.png b/src/snapshots/dot/global-facet.png new file mode 100644 index 000000000..2ac13c6d1 Binary files /dev/null and b/src/snapshots/dot/global-facet.png differ diff --git a/src/snapshots/tick/tick-x-faceted.dark.png b/src/snapshots/tick/tick-x-faceted.dark.png new file mode 100644 index 000000000..2afbac3c1 Binary files /dev/null and b/src/snapshots/tick/tick-x-faceted.dark.png differ diff --git a/src/snapshots/tick/tick-x-faceted.png b/src/snapshots/tick/tick-x-faceted.png new file mode 100644 index 000000000..cae59a4d3 Binary files /dev/null and b/src/snapshots/tick/tick-x-faceted.png differ diff --git a/src/theme/components/ActionButton.svelte b/src/theme/components/ActionButton.svelte index 5aab85fb3..a775e3f40 100644 --- a/src/theme/components/ActionButton.svelte +++ b/src/theme/components/ActionButton.svelte @@ -8,16 +8,17 @@ * @property {string} [type] - The type of the button * @property {any} to - The path to navigate to * @property {boolean} [external] - Whether the link is external + * @property {boolean} [small] - Whether to use the small button style */ /** @type {Props} */ - let { label, type = '', to, external = false } = $props(); + let { label, type = '', to, external = false, small = false } = $props(); {label} @@ -33,6 +34,13 @@ .svp-action { --at-apply: 'inline-flex items-center h-12 leading-12 rounded-6 px-6 bg-white dark:bg-zinc-8 transition-300 transition-shadow hover:shadow dark:hover:shadow-gray-6'; } + .svp-action { + --at-apply: 'inline-flex items-center h-12 leading-12 rounded-6 px-6 bg-white dark:bg-zinc-8 transition-300 transition-shadow hover:shadow dark:hover:shadow-gray-6'; + } + .svp-action--small { + --at-apply: 'h-8 leading-8 px-4 py-1 rounded-2 text-sm'; + text-decoration: none; + } .svp-action--primary { --at-apply: 'svp-gradient-bg text-white dark:text-warm-gray-8'; } diff --git a/src/theme/components/Navbar.svelte b/src/theme/components/Navbar.svelte index 2c1230be3..5c230ecc1 100644 --- a/src/theme/components/Navbar.svelte +++ b/src/theme/components/Navbar.svelte @@ -19,11 +19,9 @@
    - {#if hasError || isHome} -
    - -
    - {/if} +
    + +