From 512d7fbec0c12a498ac3e805daf5bc87b3c16bf1 Mon Sep 17 00:00:00 2001
From: Paul Pestov <10750176+paulpestov@users.noreply.github.com>
Date: Sun, 26 Nov 2023 23:07:16 +0100
Subject: [PATCH] feat: render timeline preview charts with dynamic max values

---
 src/components/Timeline.vue                   |  4 +--
 src/components/timeline/BaseTimelineChart.vue | 24 +++++++-------
 .../timeline/MetricAverageChart.vue           | 29 ++++++++--------
 src/components/timeline/MetricChart.vue       | 33 +++++++++++--------
 src/components/timeline/TimelineItem.vue      |  6 ++--
 src/helpers/metrics.ts                        | 21 ++++++++++--
 src/store/timeline-store.ts                   | 15 +++++++++
 7 files changed, 83 insertions(+), 49 deletions(-)
 create mode 100644 src/store/timeline-store.ts

diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue
index 5b5d42f..ab590b8 100644
--- a/src/components/Timeline.vue
+++ b/src/components/Timeline.vue
@@ -5,7 +5,7 @@ import Dropdown from 'primevue/dropdown'
 import { computed, onMounted, ref } from "vue"
 import { EvaluationMetrics } from '@/helpers/metrics'
 import { useI18n } from "vue-i18n"
-import type { DropdownOption, GroundTruth, Workflow } from "@/types"
+import type {DropdownOption, EvaluationResultsDocumentWide, GroundTruth, Workflow} from "@/types"
 import { DropdownPassThroughStyles } from '@/helpers/pt'
 import { store } from '@/helpers/store'
 
@@ -14,7 +14,7 @@ const gtList = ref<GroundTruth[]>([])
 const workflows = ref<Workflow[]>([])
 const selectedMetric = ref<DropdownOption | null>(null)
 const metrics = computed<DropdownOption[]>(() => Object.keys(EvaluationMetrics).map(key => ({ value: EvaluationMetrics[key], label: t(EvaluationMetrics[key]) })))
-const selectedMetricValue = computed<string>(() => selectedMetric.value?.value || EvaluationMetrics.CER_MEAN)
+const selectedMetricValue = computed<keyof EvaluationResultsDocumentWide>(() => <keyof EvaluationResultsDocumentWide>selectedMetric.value?.value || EvaluationMetrics.CER_MEAN)
 
 onMounted(async () => {
   selectedMetric.value = metrics.value[0]
diff --git a/src/components/timeline/BaseTimelineChart.vue b/src/components/timeline/BaseTimelineChart.vue
index 2323405..833c6a7 100644
--- a/src/components/timeline/BaseTimelineChart.vue
+++ b/src/components/timeline/BaseTimelineChart.vue
@@ -18,13 +18,12 @@ interface Props {
 
 const props = defineProps<Props>()
 
-const height = props.height || 60
+const height = props.height || 45
 const marginTop = 10
 const marginRight = 10
-const marginBottom = 30
+const marginBottom = 5
 const marginLeft = 40
 const _width = computed(() => props.width ?? 300)
-const _maxY = computed(() => props.maxY ?? 2)
 
 const container = ref<HTMLDivElement>()
 
@@ -42,11 +41,14 @@ function isUp(data: TimelineChartDataPoint[], higherIsUp = true) {
   else return -1
 }
 
-function render([data, startDate, endDate]) {
+function render([data, startDate, endDate, maxY]) {
   if (!data || !startDate || !endDate) return
 
   if (data.length === 0) return
 
+  if (!container.value) return
+  container.value.replaceChildren()
+
   // Declare the x (horizontal position) scale.
   const x = d3.scaleTime()
       .domain([startDate, endDate])
@@ -54,8 +56,10 @@ function render([data, startDate, endDate]) {
 
 // Declare the y (vertical position) scale.
   const y = d3.scaleLinear()
-      .domain([0, _maxY.value])
+      .domain([0, maxY])
       .range([height - marginBottom, marginTop])
+      .nice()
+
 
 // Create the SVG container.
   const svg = d3.create("svg")
@@ -75,7 +79,7 @@ function render([data, startDate, endDate]) {
   svg.append("g")
       .classed('y-axis-group', true)
       .attr("transform", `translate(${marginLeft},0)`)
-      .call(d3.axisLeft(y).ticks(1).tickSize(0).tickPadding(5))
+      .call(d3.axisLeft(y).tickValues([0, maxY]).tickSize(0).tickPadding(5))
 
   svg.select('.y-axis-group .domain').attr('stroke', colors.gray['400'])
   svg.selectAll('.y-axis-group .tick text').attr('fill', colors.gray['400'])
@@ -126,18 +130,14 @@ function render([data, startDate, endDate]) {
   setEventListeners(svg.selectAll('.chart-point'), tooltip, { useData: props.tooltipContent })
 
   // Append the SVG element.
-  if (!container.value) return
-  container.value.replaceChildren()
   container.value.append(svg.node())
 }
 
 onMounted(() => {
-  render([props.data, props.startDate, props.endDate])
+  render([props.data, props.startDate, props.endDate, props.maxY])
 })
 
-watch([() => props.data, () => props.startDate, () => props.endDate], render)
-
-
+watch([() => props.data, () => props.startDate, () => props.endDate, () => props.maxY], render)
 </script>
 
 <template>
diff --git a/src/components/timeline/MetricAverageChart.vue b/src/components/timeline/MetricAverageChart.vue
index 42adc8e..d9744c7 100644
--- a/src/components/timeline/MetricAverageChart.vue
+++ b/src/components/timeline/MetricAverageChart.vue
@@ -1,43 +1,40 @@
 <script setup lang="ts">
-import { onMounted, ref, watch } from "vue"
+import {computed, onMounted, ref, watch} from "vue"
 import api from "@/helpers/api"
 import BaseTimelineChart from "@/components/timeline/BaseTimelineChart.vue"
-import type {EvaluationResultsDocumentWide, EvaluationRun, TimelineChartDataPoint, Workflow} from "@/types"
-import { getMaxValueOfMetric } from '@/helpers/metrics'
+import type {EvaluationResultsDocumentWide, EvaluationRun, TimelineChartDataPoint} from "@/types"
 import {useI18n} from "vue-i18n";
 import { metricChartTooltipContent } from "@/helpers/metric-chart-tooltip-content";
 import OverlayPanel from "primevue/overlaypanel";
 import BaseTimelineDetailedChart from "@/components/timeline/BaseTimelineDetailedChart.vue";
+import timelineStore from "@/store/timeline-store";
 
 const { t } = useI18n()
 
 const props = defineProps<{
   gtId: string,
-  metric: string,
+  metric: keyof EvaluationResultsDocumentWide,
   startDate: Date,
   endDate: Date
 }>()
 
 const data = ref<TimelineChartDataPoint[]>([])
-const maxY = ref(2)
-const workflows = ref<Workflow[] | null>(null)
+const maxY = computed(() => timelineStore.maxValues[props.metric] ?? 0)
 const runs = ref<EvaluationRun[]>([])
 const op = ref<OverlayPanel | null>(null)
 
 onMounted(async () => {
-  const { gtId, metric } = props
-  workflows.value = await api.getWorkflows()
-
+  const { gtId } = props
   runs.value = await api.getRuns(gtId)
-
-  data.value = getTimelineData(runs.value, metric)
-  maxY.value = getMaxValueOfMetric(metric)
+  init()
 })
 
-watch(() => props.metric, async (value) => {
-  data.value = getTimelineData(runs.value, value)
-  maxY.value = getMaxValueOfMetric(value)
-})
+watch(() => props.metric, init)
+
+function init() {
+  if (!runs.value) return
+  data.value = getTimelineData(runs.value, props.metric)
+}
 
 function getTimelineData(runs: EvaluationRun[], metric: string): TimelineChartDataPoint[] {
   const datesValues = runs.reduce((acc, cur) => {
diff --git a/src/components/timeline/MetricChart.vue b/src/components/timeline/MetricChart.vue
index 75e8928..c2590fc 100644
--- a/src/components/timeline/MetricChart.vue
+++ b/src/components/timeline/MetricChart.vue
@@ -1,34 +1,37 @@
 <script setup lang="ts">
-import { onMounted, ref, watch } from "vue"
+import { computed, onMounted, ref, watch } from "vue"
 import api from "@/helpers/api"
 import BaseTimelineChart from "@/components/timeline/BaseTimelineChart.vue"
-import { getMaxValueOfMetric } from '@/helpers/metrics'
+import { extendMaxValue, getMaxValueByMetric } from '@/helpers/metrics'
 import type { EvaluationResultsDocumentWide, EvaluationRun, TimelineChartDataPoint } from "@/types"
 import { metricChartTooltipContent } from "@/helpers/metric-chart-tooltip-content"
 import OverlayPanel from 'primevue/overlaypanel'
 import BaseTimelineDetailedChart from "@/components/timeline/BaseTimelineDetailedChart.vue"
+import timelineStore from "@/store/timeline-store"
 
 const props = defineProps(['gtId', 'workflowId', 'metric', 'startDate', 'endDate'])
 const runs = ref<EvaluationRun[]>([])
 const data = ref([])
-const maxY = ref(2)
+const maxY = computed(() => timelineStore.maxValues[props.metric] ?? 0 )
 const op = ref<OverlayPanel | null>(null)
 
-
 onMounted(async () => {
-  const { gtId, workflowId, metric } = props
+  const { gtId, workflowId } = props
   runs.value = await api.getRuns(gtId, workflowId)
-  data.value = getTimelineData(runs.value, metric)
-  maxY.value = getMaxValueOfMetric(metric)
+  init()
 })
 
-watch(() => props.metric,
-    (value) => {
-      if (!runs.value) return
-      data.value = getTimelineData(runs.value, value)
-      maxY.value = getMaxValueOfMetric(value)
-    }, { immediate: true }
-)
+watch(() => props.metric, init)
+
+function init() {
+  if (!runs.value) return
+  data.value = getTimelineData(runs.value, props.metric)
+
+  const maxValueByMetric = getMaxValueByMetric(props.metric, runs.value)
+  if (maxValueByMetric > maxY.value) {
+    timelineStore.setMaxValue(props.metric, extendMaxValue(maxValueByMetric))
+  }
+}
 
 function getTimelineData(runs: EvaluationRun[], metric: keyof EvaluationResultsDocumentWide) {
   return runs.map(({ metadata, evaluation_results }) => {
@@ -44,6 +47,8 @@ function tooltipContent(d: TimelineChartDataPoint) {
   return metricChartTooltipContent(d, props.metric)
 }
 
+
+
 </script>
 
 <template>
diff --git a/src/components/timeline/TimelineItem.vue b/src/components/timeline/TimelineItem.vue
index 0b1b726..7247378 100644
--- a/src/components/timeline/TimelineItem.vue
+++ b/src/components/timeline/TimelineItem.vue
@@ -3,7 +3,7 @@ import Panel from "primevue/panel"
 import OverlayPanel from 'primevue/overlaypanel'
 import StepsAcronyms from '@/helpers/workflow-steps-acronyms'
 import MetricChart from "@/components/timeline/MetricChart.vue"
-import type { GroundTruth, Workflow, WorkflowStep } from "@/types"
+import type { EvaluationResultsDocumentWide, GroundTruth, Workflow, WorkflowStep } from "@/types"
 import MetricAverageChart from "@/components/timeline/MetricAverageChart.vue"
 import { Icon } from '@iconify/vue'
 import { ref } from "vue"
@@ -12,7 +12,7 @@ import { OverlayPanelDropdownStyles } from "@/helpers/pt"
 const props = defineProps<{
   gt: GroundTruth,
   workflows: Workflow[],
-  metric: string
+  metric: keyof EvaluationResultsDocumentWide
 }>()
 
 const op = ref<OverlayPanel>()
@@ -67,7 +67,7 @@ function hideParametersOverlay() {
       </div>
     </template>
     <template v-slot:default>
-      <div class="flex border-t border-gray-300 pt-4 px-4">
+      <div class="flex border-t border-gray-300 py-4 px-4">
         <table class="table-fixed w-full">
           <tr v-for="workflow in workflows" :key="workflow.id">
             <td class="font-semibold pe-2">{{ workflow.label }}</td>
diff --git a/src/helpers/metrics.ts b/src/helpers/metrics.ts
index f3da9b4..488697d 100644
--- a/src/helpers/metrics.ts
+++ b/src/helpers/metrics.ts
@@ -1,3 +1,5 @@
+import type { EvaluationResultsDocumentWide, EvaluationRun } from "@/types"
+
 const EvaluationMetrics = {
   CER_MEAN: 'cer_mean',
   CER_MEDIAN: 'cer_median',
@@ -8,7 +10,7 @@ const EvaluationMetrics = {
   CPU_TIME: 'cpu_time'
 }
 
-function getMaxValueOfMetric(metric: string): number {
+function getDefaultMaxValueOfMetric(metric: string): number {
   if (metric === EvaluationMetrics.CER_MEAN) return 2
   if (metric === EvaluationMetrics.CER_MEDIAN) return 2
   if (metric === EvaluationMetrics.CER_STANDARD_DEVIATION) return 2
@@ -20,7 +22,22 @@ function getMaxValueOfMetric(metric: string): number {
   else return 1
 }
 
+function getMaxValueByMetric(metric: keyof EvaluationResultsDocumentWide, runs: EvaluationRun[] = []): number {
+  const values = runs.map((run) => {
+    const value = run.evaluation_results.document_wide[metric]
+    return Array.isArray(value) ? Math.max(...value) : value ?? 0
+  }) ?? []
+
+  return Math.max(...values)
+}
+
+function extendMaxValue(value: number): number {
+  return Math.floor(value + value * 0.2)
+}
+
 export {
   EvaluationMetrics,
-  getMaxValueOfMetric
+  getMaxValueByMetric,
+  getDefaultMaxValueOfMetric,
+  extendMaxValue
 }
diff --git a/src/store/timeline-store.ts b/src/store/timeline-store.ts
new file mode 100644
index 0000000..4652169
--- /dev/null
+++ b/src/store/timeline-store.ts
@@ -0,0 +1,15 @@
+import { reactive, ref } from "vue"
+
+export default reactive<{
+  maxValues: {[metric: string]: number },
+  setMaxValue: (metric: string, value: number) => void,
+  getMaxValue: (metric: string) => number
+}>({
+  maxValues:{},
+  setMaxValue(metric: string, value: number) {
+    this.maxValues[metric] = value
+  },
+  getMaxValue(metric: string) {
+    return this.maxValues[metric] ?? 0
+  }
+})
-- 
GitLab