From 991286c124eaece8c15659009712c32a0553f9b1 Mon Sep 17 00:00:00 2001
From: Paul Pestov <10750176+paulpestov@users.noreply.github.com>
Date: Tue, 5 Dec 2023 19:47:08 +0100
Subject: [PATCH] feat: display release info in timeline charts

---
 src/components/Workflows.vue                  | 12 +++-
 .../workflows/timeline/BaseTimelineChart.vue  | 25 +++++++++
 .../timeline/BaseTimelineDetailedChart.vue    | 56 +++++++++++++++++--
 src/helpers/metric-chart-tooltip-content.ts   |  3 +-
 src/store/workflows-store.ts                  |  4 +-
 src/types/index.d.ts                          | 11 ++--
 6 files changed, 97 insertions(+), 14 deletions(-)

diff --git a/src/components/Workflows.vue b/src/components/Workflows.vue
index d14bb8e..151c568 100644
--- a/src/components/Workflows.vue
+++ b/src/components/Workflows.vue
@@ -1,4 +1,4 @@
-<script setup>
+<script setup lang="ts">
   import { onMounted, ref, watch } from "vue"
   import api from '@/helpers/api'
   import { useRouter, useRoute } from "vue-router"
@@ -10,6 +10,7 @@
   import WorkflowsTimeline from "@/components/workflows/WorkflowsTimeline.vue"
   import filtersStore from "@/store/filters-store"
   import workflowsStore from "@/store/workflows-store"
+  import type { ReleaseInfo } from "@/types";
 
   const { t } = useI18n()
 
@@ -51,9 +52,16 @@
     workflowsStore.gt = await api.getGroundTruth()
     workflowsStore.workflows = await api.getWorkflows()
 
-    loading.value = false
+    const releasesObj = workflowsStore.runs.reduce((acc, cur) => {
+      acc[cur.metadata.release_info.tag_name] = cur.metadata.release_info
+      return acc
+    }, {})
+
+    workflowsStore.releases = Object.keys(releasesObj).map(key => <ReleaseInfo>releasesObj[key])
 
     setEvalColors(workflowsStore.runs)
+
+    loading.value = false
   })
 </script>
 <template>
diff --git a/src/components/workflows/timeline/BaseTimelineChart.vue b/src/components/workflows/timeline/BaseTimelineChart.vue
index 34b3d3c..8c92c69 100644
--- a/src/components/workflows/timeline/BaseTimelineChart.vue
+++ b/src/components/workflows/timeline/BaseTimelineChart.vue
@@ -4,6 +4,7 @@ import { ref, watch, computed, onMounted } from "vue"
 import type { TimelineChartDataPoint } from "@/types"
 import { createTooltip, setEventListeners } from "@/helpers/d3/d3-tooltip"
 import colors from 'tailwindcss/colors'
+import workflowsStore from "@/store/workflows-store"
 
 
 interface Props {
@@ -41,6 +42,10 @@ function isUp(data: TimelineChartDataPoint[], higherIsUp = true) {
   else return -1
 }
 
+function renderReleases() {
+
+}
+
 function render([data, startDate, endDate, maxY]) {
   if (!data || !startDate || !endDate) return
 
@@ -129,6 +134,21 @@ function render([data, startDate, endDate, maxY]) {
 
   setEventListeners(svg.selectAll('.chart-point'), tooltip, { useData: props.tooltipContent })
 
+
+  const releasesGroup = svg
+    .append('g')
+    .classed('releases-group', true)
+
+  workflowsStore.releases.forEach(release => {
+    const xPos = x(new Date(release.published_at))
+
+    releasesGroup
+      .append("path")
+      .attr("stroke-width", 1.5)
+      .attr('stroke-dasharray', 4)
+      .attr("d", d3.line()([[xPos, y(0)], [xPos, y(maxY)]]))
+  })
+
   // Append the SVG element.
   container.value.append(svg.node())
 }
@@ -146,6 +166,11 @@ watch([() => props.data, () => props.startDate, () => props.endDate, () => props
 
 <style lang="scss">
 
+.releases-group {
+  fill: none;
+  stroke: theme('colors.gray.400');
+}
+
 .path-group {
   path {
     fill: none;
diff --git a/src/components/workflows/timeline/BaseTimelineDetailedChart.vue b/src/components/workflows/timeline/BaseTimelineDetailedChart.vue
index 889e732..e9abdbb 100644
--- a/src/components/workflows/timeline/BaseTimelineDetailedChart.vue
+++ b/src/components/workflows/timeline/BaseTimelineDetailedChart.vue
@@ -4,6 +4,7 @@ import { ref, watch, computed, onMounted } from "vue"
 import type { TimelineChartDataPoint } from "@/types"
 import { createTooltip, setEventListeners } from "@/helpers/d3/d3-tooltip"
 import colors from 'tailwindcss/colors'
+import workflowsStore from "@/store/workflows-store"
 
 
 interface Props {
@@ -43,7 +44,7 @@ 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
@@ -55,7 +56,7 @@ 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])
 
 // Create the SVG container.
@@ -136,6 +137,48 @@ function render([data, startDate, endDate]) {
 
   setEventListeners(svg.selectAll('.chart-point'), tooltip, { useData: props.tooltipContent })
 
+  const releasesGroup = svg
+      .append('g')
+      .classed('releases-group', true)
+
+  workflowsStore.releases.forEach(release => {
+    const xPos = x(new Date(release.published_at))
+
+    const releaseGroup = releasesGroup.append('g')
+
+    releaseGroup
+      .append("path")
+      .attr("stroke-width", 1.5)
+      .attr('stroke-dasharray', 4)
+      .attr("d", d3.line()([[xPos, y(0)], [xPos, y(maxY)]]))
+
+    const group = releaseGroup.append('g')
+
+    group
+      .append('rect')
+      .attr("x", xPos)
+      .attr("y", y(maxY))
+      .attr('width', 80)
+      .attr('height', 18)
+      .style("fill", 'white')
+
+    group
+      .append("text")
+      .classed('tag-name', true)
+      .attr("y", y(maxY))
+      .attr("x", xPos)
+      .attr('dy', 12)
+      .attr('dx', 5)
+      .text(release.tag_name)
+      .attr('stroke', 'none')
+      .attr('fill', colors.gray['600'])
+      .style('cursor', 'pointer')
+
+    releaseGroup.on('mouseenter', function(e) {
+      this.parentElement.appendChild(this)
+    })
+  })
+
   // Append the SVG element.
   if (!container.value) return
   container.value.replaceChildren()
@@ -143,10 +186,10 @@ function render([data, startDate, endDate]) {
 }
 
 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>
@@ -157,6 +200,11 @@ watch([() => props.data, () => props.startDate, () => props.endDate], render)
 
 <style lang="scss">
 
+.releases-group {
+  .tag-name {
+    font-size: 10px;
+  }
+}
 
 .path-group {
   path {
diff --git a/src/helpers/metric-chart-tooltip-content.ts b/src/helpers/metric-chart-tooltip-content.ts
index 5c8193e..82caaad 100644
--- a/src/helpers/metric-chart-tooltip-content.ts
+++ b/src/helpers/metric-chart-tooltip-content.ts
@@ -1,13 +1,12 @@
 import type { EvaluationResultsDocumentWide, TimelineChartDataPoint } from "@/types"
 import { createReadableMetricValue } from "@/helpers/utils"
-import { useI18n } from "vue-i18n"
 import i18n from '@/i18n'
 
 const { t } = i18n.global
 function metricChartTooltipContent(d: TimelineChartDataPoint, metric: string) {
   return `
     <div class="">
-      <span class="font-semibold">${t('date')}:</span> <span>${d.date.getDate()}.${d.date.getMonth()}.${d.date.getFullYear()}</span>
+      <span class="font-semibold">${t('date')}:</span> <span>${d.date.getDate()}.${d.date.getMonth() + 1}.${d.date.getFullYear()}</span>
     </div>
     <div class="">
       <span class="font-semibold">${t(metric)}:</span> <span>${createReadableMetricValue(<keyof EvaluationResultsDocumentWide>metric, d.value)}</span>
diff --git a/src/store/workflows-store.ts b/src/store/workflows-store.ts
index ccd9059..c443700 100644
--- a/src/store/workflows-store.ts
+++ b/src/store/workflows-store.ts
@@ -1,5 +1,5 @@
 import { reactive } from "vue"
-import type { EvaluationRun, GroundTruth, Workflow } from "@/types"
+import type { EvaluationRun, GroundTruth, ReleaseInfo, Workflow } from "@/types"
 import { mapGtId } from "@/helpers/utils"
 
 function normalizeDate(dateString: string): string {
@@ -10,6 +10,7 @@ export default reactive<{
   gt: GroundTruth[],
   workflows: Workflow[],
   runs: EvaluationRun[],
+  releases: ReleaseInfo[],
   getRuns: (gtId: string, workflowId?: string) => EvaluationRun[]
   getLatestRuns: () => EvaluationRun[],
   getGtById: (id: string) => GroundTruth | null
@@ -18,6 +19,7 @@ export default reactive<{
   gt: [],
   workflows: [],
   runs: [],
+  releases: [],
   getRuns(gtId: string, workflowId?: string) {
     return this.runs
       .filter(
diff --git a/src/types/index.d.ts b/src/types/index.d.ts
index fb682d3..02c7f3b 100644
--- a/src/types/index.d.ts
+++ b/src/types/index.d.ts
@@ -15,11 +15,6 @@ export interface Workspace {
   label: string
 }
 
-export interface ReleaseInfo {
-  id: string,
-  tage_name: string
-}
-
 export interface WorkflowStep {
   id: string,
   params: WorkflowStepParams
@@ -88,3 +83,9 @@ export interface FilterOption {
   label: string
 }
 
+export interface ReleaseInfo {
+  id: number,
+  published_at: string,
+  tag_name: string
+}
+
-- 
GitLab