aboutsummaryrefslogtreecommitdiff
path: root/data/vue
diff options
context:
space:
mode:
authorPaul Oliver <contact@pauloliver.dev>2026-03-21 01:36:24 +0100
committerPaul Oliver <contact@pauloliver.dev>2026-04-03 18:50:51 +0200
commitbda5ad2ec9fa333c8200451496d6c251abbeee19 (patch)
tree52e15344f239a023d298ee9635e8c7c0c4fdf614 /data/vue
parent9f7e70904e6c0fa650323ac5e50ebf6003da333c (diff)
Adds data server (WIP)
Diffstat (limited to 'data/vue')
-rw-r--r--data/vue/App.vue206
-rw-r--r--data/vue/Plot.vue125
-rw-r--r--data/vue/Section.vue40
3 files changed, 371 insertions, 0 deletions
diff --git a/data/vue/App.vue b/data/vue/App.vue
new file mode 100644
index 0000000..af8f1c0
--- /dev/null
+++ b/data/vue/App.vue
@@ -0,0 +1,206 @@
+<template>
+ <h1>
+ Salis data server ยป
+ <span class="opts_name">{{ opts.name }}</span>
+ </h1>
+ <form @change="on_form_change">
+ <span class="nobr">Entries (max): <input v-model.number="entries" type="text" /></span>&ensp;
+ <span class="nobr">X-axis: <select v-model="x_axis"><option v-for="axis in x_axes">{{ axis }}</option></select></span>&ensp;
+ <span class="nobr">X-low: <input v-model="x_low" type="text" /></span>&ensp;
+ <span class="nobr">X-high: <input v-model="x_high" type="text" /></span>
+ </form>
+ <Section name="Options">
+ <table>
+ <tr v-for="opt_fmt in opt_fmts">
+ <td>{{ opt_fmt[0] }}:</td>
+ <td>{{ opt_fmt[2](opts[opt_fmt[1]]) }}</td>
+ </tr>
+ </table>
+ </Section>
+ <!-- Render plots after simulation options have been loaded -->
+ <div v-if="loaded">
+ <Section v-for="(section_plots, section) in plots" :name="section" grid>
+ <Plot v-for="(_, name) in section_plots" :section="section" :name="name" />
+ </Section>
+ </div>
+</template>
+
+<script setup>
+import { onMounted, provide, ref } from 'vue'
+
+import Plot from './Plot.vue'
+import Section from './Section.vue'
+
+const root = window.location.href
+const id = v => v
+const hex = v => v !== undefined ? `0x${v.toString(16)}` : ''
+const hex_pow = v => v !== undefined ? `0x${Math.pow(2, v).toString(16)}` : ''
+const disabled = v => v ? 'disabled' : 'enabled'
+
+const opts = ref({})
+const plots = ref({})
+const tables = ref({})
+const loaded = ref(false)
+
+const entries = ref(2000)
+const x_axes = ref(['rowid'])
+const x_axis = ref(x_axes.value[0])
+const x_low = ref(hex(BigInt(0)))
+const x_high = ref(hex(BigInt(Math.pow(2, 64))))
+
+const plot_x_low = ref(0n)
+const plot_redraw = ref(false)
+const query_in_progress = ref(false)
+const data = ref([])
+
+provide('plots', plots)
+provide('entries', entries)
+provide('x_axis', x_axis)
+provide('data', data)
+
+const sanitize = (input, min, max, def, fmt) => {
+ if (isNaN(Number(input.value)) || input.value === '' || input.value < min || input.value > max) {
+ input.value = fmt(def)
+ }
+}
+
+const on_form_change = () => {
+ sanitize(entries, 1n, BigInt(Math.pow(2, 64)), 2000n, id)
+ sanitize(x_low, 0n, BigInt(Math.pow(2, 64)), 0n, hex)
+ sanitize(x_high, 1n, BigInt(Math.pow(2, 64)), BigInt(Math.pow(2, 64)), hex)
+
+ plot_x_low.value = x_low.value
+ plot_redraw.value = true
+
+ query(false)
+}
+
+const opt_fmts = [
+ ['Ancestor', 'anc', id],
+ ['Architecture', 'arch', id],
+ ['Auto-save interval', 'auto_save_pow', hex_pow],
+ ['Clones', 'clones', id],
+ ['Cores', 'cores', id],
+ ['Data push interval', 'data_push_pow', hex_pow],
+ ['Mutator flip bit', 'muta_flip', id],
+ ['Mutator range', 'muta_pow', hex_pow],
+ ['Memory vector size', 'mvec_pow', hex_pow],
+ ['Save file compression', 'no_compress', disabled],
+ ['Seed', 'seed', hex],
+]
+
+const reviver = (_, val, { source }) => {
+ if (Number.isInteger(val) && !Number.isSafeInteger(val)) {
+ try { return BigInt(source) } catch {}
+ }
+
+ return val
+}
+
+const query_table = async table => {
+ const params = {
+ table: table,
+ entries: entries.value,
+ x_axis: x_axis.value,
+ x_low: Number(plot_x_low.value),
+ x_high: Number(x_high.value),
+ }
+
+ const search_params = new URLSearchParams(params)
+ const resp_table = await fetch(root + `data?${search_params}`, { method: 'GET' })
+ const resp_json = JSON.parse(await resp_table.text(), reviver)
+
+ // Keep track of the highest x-axis value fetched so far.
+ // Future queries will set this as the minimum, which prevents re-fetching already stored data.
+ if (resp_json.length) {
+ const x_last = BigInt(resp_json.slice(-1)[0][x_axis.value] + 1)
+ plot_x_low.value = plot_x_low.value > x_last ? plot_x_low.value : x_last
+ }
+
+ return resp_json
+}
+
+const query = async (reschedule = true) => {
+ if (query_in_progress.value) return
+
+ query_in_progress.value = true
+
+ const query_results = await Promise.all(tables.value.map(query_table))
+ const query_values = Object.fromEntries(tables.value.map((key, i) => [key, query_results[i]]))
+
+ data.value = { redraw: plot_redraw.value, values: query_values }
+ plot_redraw.value = false
+ query_in_progress.value = false
+
+ if (reschedule) setTimeout(query, 5000)
+}
+
+onMounted(async () => {
+ const resp_opts = await fetch(root + 'opts', { method: 'GET' })
+ const resp_plots = await fetch(root + 'plots', { method: 'GET' })
+ const resp_tables = await fetch(root + 'tables', { method: 'GET' })
+
+ opts.value = JSON.parse(await resp_opts.text(), reviver)
+ plots.value = JSON.parse(await resp_plots.text(), reviver)
+ tables.value = JSON.parse(await resp_tables.text(), reviver)
+ loaded.value = true
+
+ // All tables should include one cycle column for each core.
+ // This allows normalizing the plots against each core's cycle count
+ // (i.e. making `cycl_#` the plots' x-axis).
+ x_axes.value.push(...Array(opts.value.cores).keys().map(i => `cycl_${i}`))
+
+ query()
+})
+</script>
+
+<style>
+html {
+ background-color: #002b36;
+ color: #586e75;
+ font-family: sans-serif;
+}
+
+h1 {
+ font-size: 20px;
+ font-weight: 600;
+}
+
+input, select {
+ background-color: #586e75;
+ border: none;
+ color: #002b36;
+ font-family: monospace;
+ font-size: 14px;
+ margin: 0 4px;
+ padding: 4px;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ height: 100%;
+ width: 100%;
+}
+
+tr:nth-child(odd) {
+ background-color: #073642;
+}
+
+td {
+ font-family: monospace;
+ font-size: 14px;
+ margin: 0;
+ padding: 0;
+}
+
+.opts_name {
+ color: #b58900;
+ font-weight: normal;
+}
+
+.nobr {
+ line-height: 32px;
+ white-space: nowrap;
+}
+</style>
diff --git a/data/vue/Plot.vue b/data/vue/Plot.vue
new file mode 100644
index 0000000..f1f0638
--- /dev/null
+++ b/data/vue/Plot.vue
@@ -0,0 +1,125 @@
+<template>
+ <div class="plot_container" :class="{ plot_maximized: maximized, plot_minimized: !maximized }">
+ <div ref="plot_ref" class="plot">
+ <button @click="plot_toggle" class="plot_button">
+ {{ maximized ? '-' : '+' }}
+ </button>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { defineProps, inject, onMounted, ref, useTemplateRef, watch } from 'vue'
+
+const props = defineProps({ section: String, name: String })
+
+const maximized = ref(false)
+
+const plots = inject('plots')
+const entries = inject('entries')
+const x_axis = inject('x_axis')
+const data = inject('data')
+
+const plot_ref = useTemplateRef('plot_ref')
+
+const plot_toggle = () => {
+ maximized.value = !maximized.value
+ Plotly.Plots.resize(plot_ref.value)
+ document.body.style.overflow = maximized.value ? 'hidden' : 'visible'
+}
+
+onMounted(() => {
+ const plot_config = plots.value[props.section][props.name]
+
+ switch (plot_config.type) {
+ case 'lines':
+ var data_defs = { mode: 'lines', line: { width: 1 }}
+ break
+ case 'stack':
+ var data_defs = { mode: 'lines', line: { width: 1 }, stackgroup: 'stackgroup' }
+ break
+ case 'stack_percent':
+ var data_defs = { mode: 'lines', line: { width: 1 }, stackgroup: 'stackgroup', groupnorm: 'percent' }
+ break
+ }
+
+ const columns = plot_config.cols
+ const data = Array.from(columns, column => ({ ...data_defs, x: [], y: [], name: column }))
+
+ Plotly.newPlot(plot_ref.value, data, {
+ legend: { font: { color: '#586e75', family: 'monospace' }, maxheight: 100, orientation: 'h' },
+ margin: { b: 32, l: 32, r: 32, t: 32 },
+ paper_bgcolor: '#002b36',
+ plot_bgcolor: '#002b36',
+ title: { font: { color: '#586e75', family: 'monospace' }, text: props.name },
+ xaxis: { gridcolor: '#073642', tickfont: { color: '#586e75' }, zerolinecolor: '#586e75' },
+ yaxis: { gridcolor: '#073642', tickfont: { color: '#586e75' }, zerolinecolor: '#586e75' },
+ }, {
+ displayModeBar: true,
+ responsive: true,
+ })
+})
+
+watch(data, new_data => {
+ const plot_config = plots.value[props.section][props.name]
+ const columns = plot_config.cols
+ const column_count = columns.length
+ const table_data = new_data.values[plot_config.table]
+ const traces = [...Array(column_count).keys()]
+ const xs = Array(column_count).fill(table_data.map(elem => elem[x_axis.value]))
+ const ys = columns.map(column => table_data.map(elem => elem[column]))
+
+ // Clear traces
+ if (new_data.redraw) {
+ const restyle = {
+ x: Array.from(columns, () => []),
+ y: Array.from(columns, () => []),
+ }
+
+ Plotly.restyle(plot_ref.value, restyle)
+ }
+
+ Plotly.extendTraces(plot_ref.value, { x: xs, y: ys }, traces, entries.value)
+})
+</script>
+
+<style>
+.plot_container {
+ background-color: #002b36;
+ display: inline-block;
+ width: 100%;
+}
+
+.plot_maximized {
+ height: 100%;
+ left: 0;
+ position: fixed;
+ top: 0;
+ z-index: 999;
+}
+
+.plot_minimized {
+ height: 400px;
+ position: relative;
+ z-index: 0;
+}
+
+.plot_button {
+ background-color: #002b36;
+ border: 1.5px solid #586e75;
+ color: #586e75;
+ cursor: pointer;
+ font-family: monospace;
+ font-size: 18px;
+ height: 26px;
+ padding: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 26px;
+}
+
+.plot {
+ height: 100%;
+}
+</style>
diff --git a/data/vue/Section.vue b/data/vue/Section.vue
new file mode 100644
index 0000000..c6464af
--- /dev/null
+++ b/data/vue/Section.vue
@@ -0,0 +1,40 @@
+<template>
+ <h2 @click="section_toggle" class="section_header">
+ {{ name }} <span class="section_button">{{ visible ? '-' : '+' }}</span>
+ </h2>
+ <div v-show="visible" :class="{ section_grid: grid }">
+ <slot></slot>
+ </div>
+</template>
+
+<script setup>
+import { defineProps, ref } from 'vue'
+
+const props = defineProps({ name: String, grid: Boolean })
+const visible = ref(true)
+const section_toggle = () => visible.value = !visible.value
+</script>
+
+<style>
+.section_header {
+ border-bottom: 1px solid #586e75;
+ cursor: pointer;
+ font-size: 18px;
+ font-weight: normal;
+}
+
+.section_button {
+ font-family: monospace;
+}
+
+.section_grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+}
+
+@media screen and (max-width: 800px) {
+ .section_grid {
+ grid-template-columns: 1fr;
+ }
+}
+</style>