Charts
This commit is contained in:
Generated
+13
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<string, StructuredFormData>);
|
||||
}
|
||||
@@ -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 @@
|
||||
<div class="mt-6">
|
||||
<div class="flex flex-col gap-6">
|
||||
{#each data.tests as test, index (test.id)}
|
||||
<AB i={index} bind:data={test} on:delete={() => deleteTest(index)} on:chanceChanged={() => onChanceChanged(test)} />
|
||||
<AB i={index} bind:data={test} bind:globalAnalytics on:delete={() => deleteTest(index)} on:chanceChanged={() => onChanceChanged(test)} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+61
-17
@@ -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'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<details class="collapse collapse-arrow rounded-lg" open={enabled} >
|
||||
<summary class="collapse-title text-xl font-medium leading-tight bg-base-300 min-h-min">
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" name="test[{i}][enabled]" class="toggle toggle-accent" bind:checked={data.enabled} />
|
||||
<input type="hidden" name="test[{i}][enabled]" value='off' />
|
||||
<span style="color: {data.color}">{data.title}</span>
|
||||
<!-- <span>{data.id}</span> -->
|
||||
<input type="range" name="test[{i}][chance]" class="range max-w-xs ml-20" min="0" max="1" step="0.01" bind:value={data.chance} on:input={() => chancePercent = Math.round(data.chance * 100)} on:input={chanceChanged} />
|
||||
<input type="text" class="input input-ghost w-16" bind:value={chancePercent} on:input={() => data.chance = chancePercent / 100} on:input={chanceChanged} />
|
||||
<!-- <span>{Math.round(data.chance * 100)}%</span> -->
|
||||
<!-- <span>{data.chance}</span> -->
|
||||
<details class="collapse-arrow collapse rounded-lg" open={enabled}>
|
||||
<summary class="collapse-title min-h-min bg-base-300 text-xl font-medium leading-tight">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex items-center basis-1/4 flex-none overflow-hidden">
|
||||
<input type="checkbox" name="test[{i}][enabled]" class="toggle-accent toggle" bind:checked={data.enabled} />
|
||||
<input type="hidden" name="test[{i}][enabled]" value="off" />
|
||||
<span class="ml-2 overflow-hidden text-ellipsis" style="color: {data.color}">{data.title}</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="range" name="test[{i}][chance]" class="range max-w-xs" min="0" max="1" step="0.01" bind:value={data.chance} on:input={() => (chancePercent = Math.round(data.chance * 100))} on:input={chanceChanged} />
|
||||
<input type="text" class="input-ghost input w-16" bind:value={chancePercent} on:input={() => (data.chance = chancePercent / 100)} on:input={chanceChanged} />
|
||||
</div>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="collapse-content flex w-full flex-col flex-wrap gap-4 bg-base-200">
|
||||
@@ -57,12 +95,13 @@
|
||||
</label>
|
||||
<input type="hidden" name="test[{i}][color]" value={data.color} />
|
||||
<ColorPicker bind:hex={data.color} isOpen={true} isPopup={false} isInput={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grow">
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Title
|
||||
<span class="label-text"
|
||||
>Title
|
||||
{#if data.title.length == 0}
|
||||
<span class="text-red-500"> (required)</span>
|
||||
{/if}
|
||||
@@ -73,7 +112,8 @@
|
||||
|
||||
<div class="form-control w-full">
|
||||
<label class="label">
|
||||
<span class="label-text">Body
|
||||
<span class="label-text"
|
||||
>Body
|
||||
{#if data.body.length == 0}
|
||||
<span class="text-red-500"> (required)</span>
|
||||
{/if}
|
||||
@@ -97,8 +137,12 @@
|
||||
{#if data.analytics}
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Analytics</h1>
|
||||
<p>Page views: {data.analytics.views}</p>
|
||||
<p>Average duration: {isNaN(data.analytics.duration) ? '???' : Math.round(data.analytics.duration)} sec</p>
|
||||
<div class="flex flex-row justify-center">
|
||||
<div bind:this={chartViews} />
|
||||
<div bind:this={chartDuration} />
|
||||
</div>
|
||||
<!-- <p>Page views: {data.analytics.views}</p>
|
||||
<p>Average duration: {isNaN(data.analytics.duration) ? '???' : Math.round(data.analytics.duration)} sec</p> -->
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { metricsDb } from "$lib/server/metrics";
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { metricsDb } from "$lib/server/metrics"
|
||||
|
||||
export async function POST({ params, cookies, request }) {
|
||||
// Update metrics page view end time
|
||||
|
||||
Reference in New Issue
Block a user