From e5b983979162cfadb628a835f666278700414e64 Mon Sep 17 00:00:00 2001
From: Paul Oliver <contact@pauloliver.dev>
Date: Thu, 16 Apr 2026 18:04:07 +0200
Subject: Improves web-ui input form usability

---
 data/vue/App.vue | 193 ++++++++++++++++++++++++++++++-------------------------
 1 file changed, 106 insertions(+), 87 deletions(-)

(limited to 'data/vue/App.vue')

diff --git a/data/vue/App.vue b/data/vue/App.vue
index cb04565..6c91964 100644
--- a/data/vue/App.vue
+++ b/data/vue/App.vue
@@ -8,16 +8,18 @@
         {{ query_in_progress ? '⧖' : '✓' }}
       </span>
     </h1>
-    <form @change="trigger_reload">
-      <span class="nobr">Entries: <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><wbr />
-      <span class="nobr">Left: <input class="input_small" v-model="hm_left" /></span><wbr />
-      <span class="nobr">Px-count: <input class="input_small" v-model="hm_px_count" /></span><wbr />
-      <span class="nobr">Px-pow: <input class="input_small" v-model="hm_px_pow" /></span><wbr />
-      <span class="reset_button" @click="reset_inputs">↻</span>
+    <form @change="on_form_change">
+      <span class="nobr">Rows: <input class="input_small" :class="{ input_touched: inputs.rows !== params.rows }" v-model="inputs.rows" /></span><wbr />
+      <span class="nobr">nth: <input class="input_small" :class="{ input_touched: inputs.nth !== params.nth }" v-model="inputs.nth" /></span><wbr />
+      <span class="nobr">Axis: <select class="input_small" :class="{ input_touched: inputs.axis !== params.axis }" v-model="inputs.axis"><option v-for="axis in axes">{{ axis }}</option></select></span><wbr />
+      <span class="nobr">Low: <input :class="{ input_touched: inputs.low !== params.low }" v-model="inputs.low" /></span><wbr />
+      <span class="nobr">High: <input :class="{ input_touched: inputs.high !== params.high }" v-model="inputs.high" /></span><wbr />
+      <span class="nobr">Left: <input class="input_small" :class="{ input_touched: inputs.left !== params.left }" v-model="inputs.left" /></span><wbr />
+      <span class="nobr">Pixels: <input class="input_small" :class="{ input_touched: inputs.pixels !== params.pixels }" v-model="inputs.pixels" /></span><wbr />
+      <span class="nobr">Px-pow: <input class="input_small" :class="{ input_touched: inputs.pixel_pow !== params.pixel_pow }" v-model="inputs.pixel_pow" /></span><wbr />
+      <span class="form_button" :class="{ form_button_disabled: !form_touched }" @click="reset_inputs">⨯</span><wbr />
+      <span class="form_button" :class="{ form_button_disabled: !form_non_default }" @click="restore_inputs">←</span><wbr />
+      <span class="form_button" @click="trigger_reload">↓</span>
     </form>
   </div>
   <Section name="Options" visible>
@@ -68,7 +70,7 @@ const opt_fmts = [
 let visible_plot_tables = []
 let visible_heatmap_tables = []
 let query_timeout = null
-let plot_x_low = 0
+let plot_low = 0
 let plot_redraw = false
 let mvec_size = 0
 
@@ -76,23 +78,22 @@ const int_max = Number.MAX_SAFE_INTEGER
 const uint32_max = (2 ** 32) - 1
 const hm_max_pixels = 2 ** 11
 
-const entries_def = 2000
-const nth_def = 1
-const x_axis_def = 'rowid'
-const x_low_def = hex(0)
-const x_high_def = hex(int_max)
-const hm_left_def = hex(0)
-const hm_px_count_def = hex(2 ** 10)
-
-const entries = ref(entries_def)
-const nth = ref(nth_def)
-const x_axes = ref(['rowid', 'step'])
-const x_axis = ref(x_axis_def)
-const x_low = ref(x_low_def)
-const x_high = ref(x_high_def)
-const hm_left = ref(hm_left_def)
-const hm_px_count = ref(hm_px_count_def)
-const hm_px_pow = ref(hex(0))
+let defaults = {
+  rows: 2000,
+  nth: 1,
+  axis: 'rowid',
+  low: hex(0),
+  high: hex(int_max),
+  left: hex(0),
+  pixels: hex(2 ** 10),
+  pixel_pow: hex(0),
+}
+
+const axes = ref(['rowid', 'step'])
+const inputs = ref({ ...defaults })
+const params = ref({ ...defaults })
+const form_touched = ref(false)
+const form_non_default = ref(false)
 
 const opts = ref({})
 const plots = ref({})
@@ -116,68 +117,76 @@ const update_visible_tables = () => {
   visible_heatmap_tables = Object.entries(heatmaps.value).filter((_, i) => heatmap_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 sanitize = (field, min, max, fmt, override = null) => {
+  if (isNaN(Number(inputs.value[field])) || inputs.value[field] === '' || inputs.value[field] < min || inputs.value[field] >= max) {
+    inputs.value[field] = override !== null ? fmt(Number(override)) : defaults[field]
+  } else {
+    inputs.value[field] = fmt(Number(inputs.value[field]))
   }
 }
 
-const max_hm_px_pow = () => Math.floor(Math.log2((mvec_size - Number(hm_left.value)) / Number(hm_px_count.value)))
-
-const trigger_reload = () => {
-  update_visible_tables()
+const max_pixel_pow = () => Math.floor(Math.log2((mvec_size - Number(inputs.value.left)) / Number(inputs.value.pixels)))
+const check_form_touched = () => form_touched.value = !Object.keys(inputs.value).every(key => inputs.value[key] === params.value[key])
+const check_form_non_default = () => form_non_default.value = !Object.keys(params.value).every(key => params.value[key] === defaults[key])
 
-  sanitize(entries, 1, uint32_max, entries_def, id)
-  sanitize(nth, 1, uint32_max, nth_def, id)
-  sanitize(x_low, 0, int_max, 0, hex)
-  sanitize(x_high, 1, int_max, int_max, hex)
+const on_form_change = () => {
+  sanitize('rows', 1, uint32_max, id)
+  sanitize('nth', 1, uint32_max, id)
+  sanitize('low', 0, int_max, hex)
+  sanitize('high', 1, int_max, hex)
 
   if (opts.value.mvec_loop) {
-    sanitize(hm_left, 0, uint32_max, 0, hex)
-    sanitize(hm_px_count, 1, hm_max_pixels, 2 ** 10, hex)
-    sanitize(hm_px_pow, 0, uint32_max, uint32_max, hex)
+    sanitize('left', 0, uint32_max, hex)
+    sanitize('pixels', 1, hm_max_pixels, hex)
+    sanitize('pixel_pow', 0, uint32_max, hex)
   } else {
-    sanitize(hm_left, 0, mvec_size, 0, hex)
-    sanitize(hm_px_count, 1, hm_max_pixels, 2 ** 10, hex)
-    sanitize(hm_px_pow, 0, max_hm_px_pow(), max_hm_px_pow(), hex)
+    sanitize('left', 0, mvec_size, hex)
+    sanitize('pixels', 1, hm_max_pixels, hex)
+    sanitize('pixel_pow', 0, max_pixel_pow(), hex, max_pixel_pow())
   }
 
-  plot_x_low = Number(x_low.value)
+  check_form_touched()
+}
+
+const trigger_reload = () => {
+  update_visible_tables()
+
+  params.value = { ...inputs.value }
+  plot_low = Number(params.value.low)
   plot_redraw = true
 
+  check_form_touched()
+  check_form_non_default()
   query()
 }
 
 const reset_inputs = () => {
-  entries.value = entries_def
-  nth.value = nth_def
-  x_axis.value = x_axis_def
-  x_low.value = x_low_def
-  x_high.value = x_high_def
-  hm_left.value = hm_left_def
-  hm_px_count.value = hm_px_count_def
-  hm_px_pow.value = max_hm_px_pow()
-
-  trigger_reload()
+  inputs.value = { ...params.value }
+  on_form_change()
+}
+
+const restore_inputs = () => {
+  inputs.value = { ...defaults }
+  on_form_change()
 }
 
-const query_table = async (table, is_heatmap, x_high_now) => {
-  const params = {
+const query_table = async (table, is_heatmap, high_now) => {
+  const url_params = {
     table: table,
-    entries: entries.value,
-    nth: nth.value,
-    x_axis: x_axis.value,
-    x_low: plot_x_low,
-    x_high: x_high_now,
+    rows: params.value.rows,
+    nth: params.value.nth,
+    axis: params.value.axis,
+    low: plot_low,
+    high: high_now,
     is_eva: is_heatmap,
     ...is_heatmap ? {
-      hm_left: Number(hm_left.value),
-      hm_px_count: Number(hm_px_count.value),
-      hm_px_pow: Number(hm_px_pow.value),
+      left: Number(params.value.left),
+      pixels: Number(params.value.pixels),
+      pixel_pow: Number(params.value.pixel_pow),
     } : {},
   }
 
-  const search_params = new URLSearchParams(params)
+  const search_params = new URLSearchParams(url_params)
   const resp_table = await fetch(root + `data?${search_params}`, { method: 'GET' })
   const text_table = await resp_table.text()
 
@@ -190,22 +199,22 @@ const query = async () => {
   clearTimeout(query_timeout)
   query_in_progress.value = true
 
-  const high_params = new URLSearchParams({ x_axis: x_axis.value })
-  const resp_x_high = await fetch(root + `x_high?${high_params}`, { method: 'GET' })
-  const text_x_high = await resp_x_high.text()
-  const json_x_high = JSON.parse(text_x_high)
-  const x_high_max = json_x_high.x_high + 1
-  const x_high_val = Number(x_high.value)
-  const x_high_now = x_high_max < x_high_val ? x_high_max : x_high_val;
+  const high_params = new URLSearchParams({ axis: params.value.axis })
+  const resp_high = await fetch(root + `high?${high_params}`, { method: 'GET' })
+  const text_high = await resp_high.text()
+  const json_high = JSON.parse(text_high)
+  const high_max = json_high.high + 1
+  const high_val = Number(params.value.high)
+  const high_now = high_max < high_val ? high_max : high_val;
 
-  const plot_query_results = await Promise.all(visible_plot_tables.map(table => query_table(table, false, x_high_now)))
-  const heatmap_query_results = await Promise.all(visible_heatmap_tables.map(table => query_table(table, true, x_high_now)))
+  const plot_query_results = await Promise.all(visible_plot_tables.map(table => query_table(table, false, high_now)))
+  const heatmap_query_results = await Promise.all(visible_heatmap_tables.map(table => query_table(table, true, high_now)))
   const plot_query_values = Object.fromEntries(visible_plot_tables.map((table, i) => [table, plot_query_results[i]]))
   const heatmap_query_values = Object.fromEntries(visible_heatmap_tables.map((table, i) => [table, heatmap_query_results[i]]))
 
   // 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.
-  plot_x_low = x_high_now
+  plot_low = high_now
 
   data.value = { redraw: plot_redraw, plot_values: plot_query_values, heatmap_values: heatmap_query_values }
   plot_redraw = false
@@ -233,12 +242,14 @@ onMounted(async () => {
   loaded.value = true
 
   mvec_size = 2 ** opts.value.mvec_pow
-  hm_px_pow.value = hex(max_hm_px_pow())
+  defaults.pixel_pow = hex(max_pixel_pow())
+  inputs.value.pixel_pow = defaults.pixel_pow
+  params.value.pixel_pow = defaults.pixel_pow
 
   // 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}`))
+  axes.value.push(...Array(opts.value.cores).keys().map(i => `cycl_${i}`))
 })
 
 watch(loaded, _ => {
@@ -251,11 +262,7 @@ watch(loaded, _ => {
 
 provide('plots', plots)
 provide('heatmaps', heatmaps)
-provide('entries', entries)
-provide('x_axis', x_axis)
-provide('hm_left', hm_left)
-provide('hm_px_count', hm_px_count)
-provide('hm_px_pow', hm_px_pow)
+provide('params', params)
 provide('data', data)
 provide('trigger_reload', trigger_reload)
 </script>
@@ -329,14 +336,19 @@ td {
   width: 80px;
 }
 
-.reset_button {
+.input_touched {
+  background-color: #b58900;
+}
+
+.form_button {
   background-color: black;
-  border: 1.5px solid gray;
-  color: gray;
+  border: 1.5px solid #b58900;
+  color: #b58900;
   cursor: pointer;
   display: inline-block;
   font-family: monospace;
   font-size: 14px;
+  font-weight: bold;
   height: 16px;
   line-height: 16px;
   margin: 0 4px;
@@ -344,4 +356,11 @@ td {
   text-align: center;
   width: 16px;
 }
+
+.form_button_disabled {
+  border-color: gray;
+  color: gray;
+  cursor: default;
+  opacity: 0.5;
+}
 </style>
-- 
cgit v1.3

