diff options
Diffstat (limited to 'data/vue')
| -rw-r--r-- | data/vue/App.vue | 242 | ||||
| -rw-r--r-- | data/vue/Plot.vue | 133 | ||||
| -rw-r--r-- | data/vue/Section.vue | 40 |
3 files changed, 415 insertions, 0 deletions
diff --git a/data/vue/App.vue b/data/vue/App.vue new file mode 100644 index 0000000..114857a --- /dev/null +++ b/data/vue/App.vue @@ -0,0 +1,242 @@ +<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="on_form_change"> + <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"> + <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 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 } 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], +] + +const opts = ref({}) +const plots = ref({}) +const tables = 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 plot_x_low = ref(0n) +const plot_redraw = ref(false) +const query_in_progress = ref(false) +const data = ref([]) + +const top_pad = useTemplateRef('top_pad') +const top_bar = useTemplateRef('top_bar') + +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(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.value = x_low.value + plot_redraw.value = true + + query(false) +} + +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.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, 10000) +} + +onMounted(async () => { + window.onresize = _ => pad_top_bar() + pad_top_bar() + + // Fetch initial data + 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: 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..41d78d5 --- /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"> + {{ 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 = () => { + maximized.value = !maximized.value + Plotly.Plots.resize(plot_ref.value) + document.body.style.overflow = maximized.value ? 'hidden' : 'visible' +} + +const prevent_focus_scroll = () => { + 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_focus_scroll() +}) + +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..6977ac6 --- /dev/null +++ b/data/vue/Section.vue @@ -0,0 +1,40 @@ +<template> + <h2 class="section_header" @click="section_toggle"> + {{ name }} <span class="section_button">{{ visible ? '-' : '+' }}</span> + </h2> + <div :class="{ section_grid: grid }" v-show="visible"> + <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> |
