diff --git a/package-lock.json b/package-lock.json index 94d29a1..96daa07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@vavite/node-loader": "^1.8.1", "font-color-contrast": "^11.1.0", "lodash": "^4.17.21", + "plotly.js-dist": "^2.24.2", "qs": "^6.11.2", "rollup-plugin-copy": "^3.4.0", "svelte-awesome-color-picker": "^2.4.4", @@ -21,6 +22,7 @@ "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/kit": "^1.5.0", "@types/lodash": "^4.14.195", + "@types/plotly.js": "^2.12.18", "@types/qs": "^6.9.7", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", @@ -716,6 +718,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.6.tgz", "integrity": "sha512-GQBWUtGoefMEOx/vu+emHEHU5aw6JdDoEtZhoBrHFPZbA/YNRFfN996XbBASEWdvmLSLyv9FKYppYGyZjCaq/g==" }, + "node_modules/@types/plotly.js": { + "version": "2.12.18", + "resolved": "https://registry.npmjs.org/@types/plotly.js/-/plotly.js-2.12.18.tgz", + "integrity": "sha512-ff+CIEWnqZNjZqHtQZvkEAVuLs9fkm1f54QnPVmgoET7wMHdSqUka2hasVN4e5yfHD05YwGjsAtCseewJh/BMw==", + "dev": true + }, "node_modules/@types/pug": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", @@ -2761,6 +2769,11 @@ "node": ">= 6" } }, + "node_modules/plotly.js-dist": { + "version": "2.24.2", + "resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.24.2.tgz", + "integrity": "sha512-J5Jg6mAkMwzjHsfLlZaZiS11ntKvdkyP5C1Re2SFL5dXL7Nb66BGGpu3UopbIUaopLVJBiYu9kvOfUTjy0rHFQ==" + }, "node_modules/postcss": { "version": "8.4.24", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", diff --git a/package.json b/package.json index 4419389..8ccdf26 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/kit": "^1.5.0", "@types/lodash": "^4.14.195", + "@types/plotly.js": "^2.12.18", "@types/qs": "^6.9.7", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", @@ -42,6 +43,7 @@ "@vavite/node-loader": "^1.8.1", "font-color-contrast": "^11.1.0", "lodash": "^4.17.21", + "plotly.js-dist": "^2.24.2", "qs": "^6.11.2", "rollup-plugin-copy": "^3.4.0", "svelte-awesome-color-picker": "^2.4.4", diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 3cf6c86..7ec4587 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -2,7 +2,6 @@ import { metricsDb } from '$lib/server/metrics'; import { tests as testsStore, type Test, userTests } from '$lib/server/tests' import { get } from 'svelte/store' - export async function load({ cookies, params }) { // Set a random session ID cookie if there is none, so we can uniquely // identify the user and not randomly change their experience on every page load. diff --git a/src/routes/dash/+page.server.ts b/src/routes/dash/+page.server.ts index 28a8358..a1d6cc8 100644 --- a/src/routes/dash/+page.server.ts +++ b/src/routes/dash/+page.server.ts @@ -80,29 +80,4 @@ export const actions = { return { success: true } } -} - -type StructuredFormData = - | string - | boolean - | number - | File - | StructuredFormData[]; - -function formBody(body: FormData) { - return [...body.entries()].reduce((data, [k, v]) => { - let value: StructuredFormData = v; - if (v === "true") value = true; - if (v === "false") value = false; - if (!isNaN(Number(v))) value = Number(v); - - // For grouped fields like multi-selects and checkboxes, we need to - // store the values in an array. - if (k in data) { - const val = data[k]; - value = Array.isArray(val) ? [...val, value] : [val, value]; - } - data[k] = value; - return data; - }, {} as Record); } \ No newline at end of file diff --git a/src/routes/dash/+page.svelte b/src/routes/dash/+page.svelte index a777473..6f54f61 100644 --- a/src/routes/dash/+page.svelte +++ b/src/routes/dash/+page.svelte @@ -2,7 +2,6 @@ import { enhance } from '$app/forms' import { page } from '$app/stores' import type { Test } from '$lib/server/tests' - import { tick } from 'svelte' import AB from './AB.svelte' export let data @@ -37,6 +36,27 @@ otherTests.forEach((t) => (t.chance = Math.round(Math.max(0, Math.min(t.chance + diffPerTest, 1) * 100)) / 100)) } + // Calculate max views and duration across all tests so we can scale the charts accordingly + let globalAnalytics: { + maxViews: number + maxDuration: number + } + $: globalAnalytics = data.tests.reduce( + (acc, test) => { + if (test.analytics) { + if (test.analytics?.views > acc.maxViews) { + acc.maxViews = test.analytics.views + } + if (test.analytics?.duration > acc.maxDuration) { + acc.maxDuration = test.analytics.duration + } + } + return acc + }, + { maxViews: 0, maxDuration: 0 } + ) + + // Prevent accidental form submission on enter function preventSubmit(e: KeyboardEvent) { if (e.key === 'Enter') { e.preventDefault() @@ -52,7 +72,7 @@
{#each data.tests as test, index (test.id)} - deleteTest(index)} on:chanceChanged={() => onChanceChanged(test)} /> + deleteTest(index)} on:chanceChanged={() => onChanceChanged(test)} /> {/each}
diff --git a/src/routes/dash/AB.svelte b/src/routes/dash/AB.svelte index e850ab6..54bdff4 100644 --- a/src/routes/dash/AB.svelte +++ b/src/routes/dash/AB.svelte @@ -3,12 +3,13 @@ import Editor from '@tinymce/tinymce-svelte' import { createEventDispatcher } from 'svelte' import type { Test } from '$lib/server/tests' - import { tick } from 'svelte' + import { onMount } from 'svelte' const dispatch = createEventDispatcher() export let i: number export let data: Test & { analytics?: { views: number; duration: number } } + export let globalAnalytics: { maxViews: number; maxDuration: number } function deleteSelf(this: HTMLButtonElement) { if (this.innerHTML !== 'Delete?') { @@ -33,19 +34,56 @@ } const enabled = data.enabled + + let chartViews: HTMLDivElement + let chartDuration: HTMLDivElement + onMount(async () => { + const Plotly = await import('plotly.js-dist') + Plotly.newPlot(chartViews, [ + { + domain: { x: [0, 1], y: [0, 1] }, + value: data.analytics?.views, + title: { text: 'Page Views' }, + type: 'indicator', + mode: 'gauge+number', + gauge: { + axis: { range: [0, globalAnalytics.maxViews] }, + bgcolor: 'white', + } + }, + ], { + width: 400, height: 230, margin: { t: 0, b: 0 }, paper_bgcolor: 'transparent' + }) + Plotly.newPlot(chartDuration, [ + { + domain: { x: [0, 1], y: [0, 1] }, + value: data.analytics?.duration, + title: { text: 'Average Duration (s)' }, + type: 'indicator', + mode: 'gauge+number', + gauge: { + axis: { range: [0, globalAnalytics.maxDuration] }, + bgcolor: 'white', + } + }, + ], { + width: 400, height: 230, margin: { t: 0, b: 0 }, paper_bgcolor: 'transparent' + }) + }) -
- -
- - - {data.title} - - chancePercent = Math.round(data.chance * 100)} on:input={chanceChanged} /> - data.chance = chancePercent / 100} on:input={chanceChanged} /> - - +
+ +
+
+ + + {data.title} +
+
+ (chancePercent = Math.round(data.chance * 100))} on:input={chanceChanged} /> + (data.chance = chancePercent / 100)} on:input={chanceChanged} /> +
@@ -57,12 +95,13 @@ -
+