diff options
| author | Paul Oliver <contact@pauloliver.dev> | 2026-03-21 01:36:24 +0100 |
|---|---|---|
| committer | Paul Oliver <contact@pauloliver.dev> | 2026-04-06 06:13:34 +0200 |
| commit | 52d836f146e080f83688fa67f56fb31dc3d1f5b8 (patch) | |
| tree | f84dd4929bae23cc7f25ed21be9ded4107af950d /data/vue | |
| parent | 9f7e70904e6c0fa650323ac5e50ebf6003da333c (diff) | |
Adds data server (WIP)data_improvements
Diffstat (limited to 'data/vue')
| -rw-r--r-- | data/vue/App.vue | 254 | ||||
| -rw-r--r-- | data/vue/Plot.vue | 133 | ||||
| -rw-r--r-- | data/vue/Section.vue | 47 |
3 files changed, 434 insertions, 0 deletions
diff --git a/data/vue/App.vue b/data/vue/App.vue new file mode 100644 index 0000000..81b5763 --- /dev/null +++ b/data/vue/App.vue @@ -0,0 +1,254 @@ +<template> + <div ref="top_pad"></div> + <div class="top_bar" ref="top_bar"> + <h1> + Salis data server » + <span class="opts_name"> + {{ opts.name }} + {{ query_in_progress ? '⧖' : '✓' }} + </span> + </h1> + <form @change="trigger_reload"> + <span class="nobr">Entries (max): <input class="input_small" v-model="entries" /></span><wbr /> + <span class="nobr">nth: <input class="input_small" v-model="nth" /></span><wbr /> + <span class="nobr">X-axis: <select class="input_small" v-model="x_axis"><option v-for="axis in x_axes">{{ axis }}</option></select></span><wbr /> + <span class="nobr">X-low: <input v-model="x_low" /></span><wbr /> + <span class="nobr">X-high: <input v-model="x_high" /></span> + </form> + </div> + <Section name="Options" visible> + <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 :name="section" grid ref="plot_sections" :visible="section === 'General'" triggers_reload v-for="(section_plots, section) in plots"> + <Plot :name="name" :section="section" v-for="(_, name) in section_plots" /> + </Section> + </div> +</template> + +<script setup> +import { onMounted, provide, useTemplateRef, ref, watch } 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 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], +] + +let visible_tables = [] +let query_timeout = null +let plot_x_low = 0n +let plot_redraw = false + +const opts = ref({}) +const plots = ref({}) +const loaded = ref(false) + +const entries = ref(2000) +const nth = ref(BigInt(1)) +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 query_in_progress = ref(false) +const data = ref([]) + +const top_pad = useTemplateRef('top_pad') +const top_bar = useTemplateRef('top_bar') +const plot_sections = useTemplateRef('plot_sections') + +const update_visible_tables = () => { + const section_visibility = plot_sections.value.map(section => section.visible) + visible_tables = Object.entries(plots.value).filter((_, i) => section_visibility[i]).map((section, _) => [...new Set(Object.entries(section[1]).map(plot => plot[1].table))]).flat() +} + +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 trigger_reload = () => { + update_visible_tables() + + sanitize(entries, 1n, BigInt(Math.pow(2, 64)), 2000n, id) + sanitize(nth, 1n, BigInt(Math.pow(2, 64)), 1n, 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 = x_low.value + plot_redraw = true + + query() +} + +const pad_top_bar = () => { + top_pad.value.style.height = `${Math.round(top_bar.value.getBoundingClientRect().height)}px` +} + +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, + nth: nth.value, + x_axis: x_axis.value, + x_low: Number(plot_x_low), + 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 = plot_x_low > x_last ? plot_x_low : x_last + } + + return resp_json +} + +const query = async () => { + if (query_in_progress.value) return + + clearTimeout(query_timeout) + query_in_progress.value = true + + const query_results = await Promise.all(visible_tables.map(query_table)) + const query_values = Object.fromEntries(visible_tables.map((key, i) => [key, query_results[i]])) + + data.value = { redraw: plot_redraw, values: query_values } + plot_redraw = false + + query_in_progress.value = false + query_timeout = setTimeout(query, 10000) +} + +onMounted(async () => { + window.onresize = _ => pad_top_bar() + pad_top_bar() + + const resp_opts = await fetch(root + 'opts', { method: 'GET' }) + const resp_plots = await fetch(root + 'plots', { method: 'GET' }) + + opts.value = JSON.parse(await resp_opts.text(), reviver) + plots.value = JSON.parse(await resp_plots.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}`)) +}) + +watch(loaded, _ => { + update_visible_tables() + query() +}, { flush: 'post' }) + +provide('plots', plots) +provide('entries', entries) +provide('x_axis', x_axis) +provide('data', data) +provide('trigger_reload', trigger_reload) +</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: 2px; +} + +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; +} + +.top_bar { + background-color: #073642; + left: 0; + padding: 8px; + position: fixed; + top: 0; + width: 100%; + z-index: 1; +} + +.opts_name { + color: #b58900; + font-weight: normal; +} + +.nobr { + line-height: 32px; + margin-right: 16px; + white-space: nowrap; +} + +.input_small { + width: 80px; +} +</style> diff --git a/data/vue/Plot.vue b/data/vue/Plot.vue new file mode 100644 index 0000000..3c08d73 --- /dev/null +++ b/data/vue/Plot.vue @@ -0,0 +1,133 @@ +<template> + <div class="plot_container" :class="{ plot_maximized: maximized, plot_minimized: !maximized }" ref="plot_container"> + <div class="plot" ref="plot_ref"> + <button class="plot_button" @click="plot_toggle_maximize"> + {{ 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 plot_ref = useTemplateRef('plot_ref') +const plot_container = useTemplateRef('plot_container') + +const plots = inject('plots') +const entries = inject('entries') +const x_axis = inject('x_axis') +const data = inject('data') + +const plot_toggle_maximize = () => { + maximized.value = !maximized.value + Plotly.Plots.resize(plot_ref.value) + document.body.style.overflow = maximized.value ? 'hidden' : 'visible' +} + +const prevent_plotly_buttons_tab_focus = () => { + const focusableElements = plot_container.value.querySelectorAll('a, button, input, select') + focusableElements.forEach(elem => elem.setAttribute('tabindex', '-1')) +} + +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' }, text: props.name, x: 0, xref: 'paper' }, + xaxis: { gridcolor: '#073642', tickfont: { color: '#586e75' }, zerolinecolor: '#586e75' }, + yaxis: { gridcolor: '#073642', tickfont: { color: '#586e75' }, zerolinecolor: '#586e75' }, + }, { + displayModeBar: true, + responsive: true, + }) + + prevent_plotly_buttons_tab_focus() +}) + +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..f0202c0 --- /dev/null +++ b/data/vue/Section.vue @@ -0,0 +1,47 @@ +<template> + <h2 class="section_header" @click="section_toggle_visible"> + {{ name }} <span class="section_button">{{ visible ? '-' : '+' }}</span> + </h2> + <div :class="{ section_grid: grid }" v-if="visible"> + <slot></slot> + </div> +</template> + +<script setup> +import { defineProps, inject, ref } from 'vue' + +const props = defineProps({ name: String, grid: Boolean, visible: Boolean, triggers_reload: Boolean }) +const visible = ref(props.visible) +const trigger_reload = inject('trigger_reload') + +const section_toggle_visible = () => { + visible.value = !visible.value + if (props.triggers_reload) trigger_reload() +} + +defineExpose({ visible }) +</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> |
