From ef97998f37f53d32ef895bc3aef1473014884905 Mon Sep 17 00:00:00 2001
From: Paul Pestov <10750176+paulpestov@users.noreply.github.com>
Date: Sat, 28 Jan 2023 23:28:28 +0100
Subject: [PATCH] Generalize dynamic coloring for metrics, fix display for
 additional metrics

---
 src/assets/app.scss                         |  21 +++
 src/components/Workflows.vue                |   5 +-
 src/components/workflows/WorkflowsList.vue  | 156 +++++++++--------
 src/components/workflows/WorkflowsTable.vue |  61 ++++---
 src/helpers/api.js                          | 181 +-------------------
 src/helpers/eval-colors.js                  |  74 --------
 src/helpers/shorten-cer.js                  |  10 +-
 src/helpers/store.js                        |  16 +-
 src/helpers/utils.js                        |  73 ++++++++
 src/locales/de.json                         |   4 +-
 src/locales/en.json                         |   5 +-
 11 files changed, 241 insertions(+), 365 deletions(-)
 delete mode 100644 src/helpers/eval-colors.js
 create mode 100644 src/helpers/utils.js

diff --git a/src/assets/app.scss b/src/assets/app.scss
index d134308..2cee20b 100644
--- a/src/assets/app.scss
+++ b/src/assets/app.scss
@@ -80,3 +80,24 @@ h3 {
     ----margin-right: 0.5rem;
   }
 }
+
+.collapsible {
+  .collapsible-item {
+    display: flex;
+    flex-direction: column;
+  }
+  .collapsible-header {
+    font-size: var(--font-size--md);
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    padding-right: 32px !important;
+    display: inline !important;
+    overflow: hidden;
+
+    .icon {
+      position: absolute !important;
+      right: 10px;
+      top: 12px;
+    }
+  }
+}
diff --git a/src/components/Workflows.vue b/src/components/Workflows.vue
index 3cbfd1a..a15fb2b 100644
--- a/src/components/Workflows.vue
+++ b/src/components/Workflows.vue
@@ -25,7 +25,7 @@
   import WorkflowsList from "@/components/workflows/WorkflowsList.vue";
   import WorkflowsTable from "@/components/workflows/WorkflowsTable.vue";
   import { useI18n } from "vue-i18n";
-  import { setEvalColors } from "@/helpers/eval-colors";
+  import { setEvalColors } from "@/helpers/utils";
   import { store } from "@/helpers/store";
   import MultiFilter from "@/components/workflows/MultiFilter.vue";
 
@@ -68,7 +68,10 @@
     store.setRepos(await api.getProjects());
 
     data.value = await api.getWorkflows();
+    store.setEvaluations(data.value);
+
     defs.value = await api.getEvalDefinitions();
+    store.setMetricDefinitions(defs.value);
 
     filteredData.value = data.value;
 
diff --git a/src/components/workflows/WorkflowsList.vue b/src/components/workflows/WorkflowsList.vue
index 5c21db4..b46a5c8 100644
--- a/src/components/workflows/WorkflowsList.vue
+++ b/src/components/workflows/WorkflowsList.vue
@@ -2,7 +2,7 @@
   <div>
     <div class="_display:flex _margin-bottom:4">
       <div class="_display:flex _align-items:center _margin-left:auto">
-        <p class="_margin-right:2">{{ $t('sort_by')}}:</p>
+        <p class="_margin-right:2">{{ $t('sort_by') }}:</p>
         <i-select
             v-model="sortBy"
             :options="sortOptions"
@@ -20,82 +20,94 @@
           <i-badge class="bg-gray-300 text-gray-700 _margin-left:2">Model: {{ item.metadata.workflow_model }}</i-badge>
           <template v-if="item.metadata.document_metadata">
             <i-badge
-              v-for="font in item.metadata.document_metadata.data_properties.fonts"
-              :key="font"
-              class="_margin-left:1 bg-gray-300 text-gray-700">
-              {{font}}
+                v-for="font in item.metadata.document_metadata.data_properties.fonts"
+                :key="font"
+                class="_margin-left:1 bg-gray-300 text-gray-700">
+              {{ font }}
             </i-badge>
           </template>
         </div>
       </template>
       <template #default>
         <i-row>
-          <i-column xs="7">
-            <i-row class="_margin-top:2">
-              <i-column>
-                <template v-if="item.metadata.gt_workspace">
-                  <i-collapsible class="_font-size:sm">
-                    <i-collapsible-item :title="item.metadata.gt_workspace.label">
-                      <i-row v-if="item.metadata.document_metadata.data_properties">
-                        <i-column>
-                          <p class="_font-weight:bold">{{ $t('number_of_pages') }}:</p>
-                          <p>{{ item.metadata.document_metadata.data_properties.number_of_pages }}</p>
-                          <p class="mt-2 _font-weight:bold">{{ $t('publication_year') }}:</p>
-                          <p> {{ item.metadata.document_metadata.data_properties.publication_year }}</p>
-                        </i-column>
-                        <i-column>
-                          <p class="_font-weight:bold">{{ $t('layout') }}:</p>
-                          <p>{{ item.metadata.document_metadata.data_properties.layout }}</p>
-                        </i-column>
-                      </i-row>
-                      <i-row v-else><i-column>{{ $t('no_document_metadata')}}</i-column></i-row>
-                    </i-collapsible-item>
-                  </i-collapsible>
-                </template>
-                <template v-else>
-                  <p class="_font-weight:bold text-gray-400 mt-3">{{$t('no_gt_workspace')}}</p>
-                </template>
+          <i-column xs="5">
+            <i-row>
+              <i-column xs="3" class="_font-weight:semibold">{{ $t('document') }}:</i-column>
+              <i-column xs="9" v-if="item.metadata.gt_workspace">
+                <i-collapsible size="md" class="_font-size:sm _flex-grow:1">
+                  <i-collapsible-item :title="item.metadata.gt_workspace.label">
+                    <i-row v-if="item.metadata.document_metadata.data_properties">
+                      <i-column>
+                        <p class="_font-weight:bold">{{ $t('number_of_pages') }}:</p>
+                        <p>{{ item.metadata.document_metadata.data_properties.number_of_pages }}</p>
+                        <p class="mt-2 _font-weight:bold">{{ $t('publication_year') }}:</p>
+                        <p> {{ item.metadata.document_metadata.data_properties.publication_year }}</p>
+                      </i-column>
+                      <i-column>
+                        <p class="_font-weight:bold">{{ $t('layout') }}:</p>
+                        <p>{{ item.metadata.document_metadata.data_properties.layout }}</p>
+                      </i-column>
+                    </i-row>
+                    <i-row v-else>
+                      <i-column>{{ $t('no_document_metadata') }}</i-column>
+                    </i-row>
+                  </i-collapsible-item>
+                </i-collapsible>
               </i-column>
-              <i-column class="_display:flex-1">
-                <i-collapsible class="_font-size:sm">
+              <template v-else>
+                <p class="_font-weight:bold text-gray-400 mt-3">{{ $t('no_gt_workspace') }}</p>
+              </template>
+            </i-row>
+
+            <i-row class="_display:flex _margin-top:1">
+              <i-column xs="3" class="_font-weight:semibold">{{ $t('workflow') }}:</i-column>
+              <i-column xs="9">
+                <i-collapsible size="md" class="_font-size:sm _flex-grow:1">
                   <i-collapsible-item :title="item.metadata.ocr_workflow?.label || $t('unknown_workflow')">
                     <div class="_display:flex _flex-direction:column" v-if="item.metadata.workflow_steps">
-                      <span class="_margin-bottom:1">{{ $t('workflow_steps')}}:</span>
-                      <span v-for="({id, url}, i) in item.metadata.workflow_steps" :key="id">
-                        <span>{{ i + 1 }}. </span>
-                        <i-badge size="lg" class="_font-size:sm _margin-bottom:1/2">
-                          <a v-if="url" :href="url" target="_blank" :title="$t('external_repo_url')" class="_display:flex _align-items:flex-end">
-                            {{ id }}
+                      <div class="_margin-bottom:2" v-for="({id, url, params }, i) in item.metadata.workflow_steps" :key="id">
+                        <div class="_display:flex _align-items:center">
+                          <span class="_margin-right:1">{{ i + 1 }}. </span>
+                          <a v-if="url" :href="url" target="_blank" :title="$t('external_repo_url')"
+                             class="_display:flex _align-items:center _flex-shrink:0">
+                            <span class="_font-weight:semibold _font-size:md">{{ id }}</span>
                             <i class="repo-icon _margin-left:1/3" v-html="getIcon('external-link')"></i>
-                          </a>
-                          <template v-else>{{ id }}</template>
-                        </i-badge>
-                      </span>
+                          </a></div>
+                        <div class="_display:flex _flex-wrap:wrap _align-items:flex-start _margin-top:1">
+                          <div
+                            v-for="{ name, value } in params"
+                            :key="name"
+                            class="_margin-bottom:1 _margin-right:1 _display:flex _align-items:flex-start"
+                            style="line-height:1.2"
+                          >
+                            <span class="_border-top-left-radius _border-bottom-left-radius _background:gray-20 _color:gray-70 _font-weight:semibold _padding-x:1 _padding-y:1/2">{{name }}</span>
+                            <span class="_border-top-right-radius _border-bottom-right-radius _background:gray-10 _color:gray-70 _padding-x:1 _padding-y:1/2">{{value }}</span>
+                          </div>
+                        </div>
+                      </div>
                     </div>
                     <template v-else>
-                      <span class="_font-weight:bold">{{$t('no_ocr_workflow')}}</span>
+                      <span class="_font-weight:bold">{{ $t('no_ocr_workflow') }}</span>
                     </template>
                   </i-collapsible-item>
                 </i-collapsible>
               </i-column>
             </i-row>
           </i-column>
-          <i-column xs="5" class="_margin-left:auto">
-            <i-row>
-              <i-column class="_display:flex _justify-content:center" v-for="(evalKey, i) in evals" :key="i">
-                <span class="_font-weight:bold _font-size:xs">{{defs[evalKey] ? defs[evalKey].label : evalKey}}</span>
-              </i-column>
-            </i-row>
+          <i-column xs="7" class="_margin-left:auto">
             <i-row>
-              <i-column v-for="({ name, value }, i) in item.evaluations" :key="i" class="_text-align:center">
-              <i-badge
-                  size="lg"
-                  class="metric _cursor:pointer _padding-x:1"
-                  :class="getEvalColor(name, value)" :title="value">
-                <template v-if=" name === 'cer'">{{ shortenCER(value) }}</template>
-                <template v-else-if="name === 'cer_min_max'">{{ shortenCER(value[0]) + '/' + shortenCER(value[1])}}</template>
-                <template v-else>{{ value }}</template>
-              </i-badge>
+              <i-column v-for="({ name, value }, i) in item.evaluations" :key="i"
+                        class="_display:flex _flex-direction:column _align-items:center _padding-x:1/2">
+                <span
+                    class="_font-weight:bold _font-size:xs _margin-bottom:1">{{
+                    defs[name] ? defs[name].label : name
+                  }}</span>
+                <i-badge
+                    size="lg"
+                    class="metric _cursor:pointer _padding-x:1"
+                    :class="getEvalColor(name, value)" :title="value">
+                  {{ createReadableMetricValue(name, value) }}
+                </i-badge>
               </i-column>
             </i-row>
           </i-column>
@@ -107,10 +119,11 @@
 
 <script setup>
 import { ref, onMounted, watch } from "vue";
-import { getEvalColor } from "@/helpers/eval-colors";
 import { useI18n } from "vue-i18n";
 import { getIcon } from "@/helpers/icon";
 import { store } from "@/helpers/store";
+import { createReadableMetricValue, getEvalColor } from "@/helpers/utils";
+
 
 const props = defineProps(['data', 'defs']);
 const list = ref([]);
@@ -215,14 +228,18 @@ const sortByCERMax = (order = 'asc') => {
 
 const mapMetadata = ({
   workflow_model = t('no_workflow_model'),
-  document_metadata = {
-    fonts: []
-  },
+  document_metadata = { fonts: [] },
   gt_workspace = null,
   ocr_workflow = null,
-  workflow_steps = null
+  workflow_steps = {}
 }) => {
-  workflow_steps = workflow_steps.map(step => ({ id: step, url: getRepoUrl(step) }));
+  workflow_steps = workflow_steps
+      .map(step => {
+        const id = Object.keys(step)[0];
+        const params = Object.keys(step[id]).map(paramKey => ({ name: paramKey, value: step[id][paramKey] }));
+
+        return { id, url: getRepoUrl(id), params };
+      });
   return {
     workflow_model,
     document_metadata,
@@ -240,7 +257,7 @@ const mapEvaluationResults = ({ document_wide = [] }) => {
 };
 
 const setListData = (data) => {
-  list.value = data.map(({ label, evaluation_results = [], metadata }) => ({
+  list.value = data.map(({ label, evaluation_results = {}, metadata }) => ({
     label,
     metadata: mapMetadata(metadata),
     evaluations: mapEvaluationResults(evaluation_results)
@@ -264,13 +281,9 @@ const setEvals = (data) => {
           : [];
 };
 
-const shortenCER = (value) => {
-  return Math.round(value * 1000) / 1000;
-};
-
 onMounted(() => {
- setEvals(props.data);
- setListData(props.data);
+  setEvals(props.data);
+  setListData(props.data);
 });
 
 watch(() => props.data, () => {
@@ -296,6 +309,7 @@ watch(() => props.data, () => {
 }
 
 .arrow-icon, .repo-icon {
+  position: relative;
   width: 16px;
   height: 16px;
 
diff --git a/src/components/workflows/WorkflowsTable.vue b/src/components/workflows/WorkflowsTable.vue
index 2acf347..1c2332d 100644
--- a/src/components/workflows/WorkflowsTable.vue
+++ b/src/components/workflows/WorkflowsTable.vue
@@ -2,7 +2,7 @@
   <div>
     <div class="_display:flex _margin-bottom:4" v-if="evals.length > 0">
       <div class="_display:flex _align-items:center _margin-left:auto">
-        <p class="_margin-right:2">{{ $t('group_by')}}:</p>
+        <p class="_margin-right:2">{{ $t('group_by') }}:</p>
         <i-select
             v-model="sortBy"
             :options="sortOptions"
@@ -15,17 +15,17 @@
     </div>
     <i-table v-if="evals.length > 0" class="_width:100%" condensed border="true">
       <thead>
-        <tr>
+      <tr>
         <th class="_padding-left:2">{{ sortBy.value === 'documents' ? $t('documents') : $t('workflows') }}</th>
         <th class="_padding-left:2">{{ sortBy.value === 'documents' ? $t('workflows') : $t('documents') }}</th>
         <th v-for="(evalKey, i) in evals" :key="i">
           <span class="def-label _display:flex _align-items:center _justify-content:center _cursor:pointer">
-            {{defs[evalKey] ? defs[evalKey].label : evalKey}}
-            <i-icon name="ink-info" />
+            {{ defs[evalKey] ? defs[evalKey].label : evalKey }}
+            <i-icon name="ink-info"/>
             <div class="def-tooltip">
               <i-card>
                 {{ defs[evalKey] ? defs[evalKey].short_descr : $t('no_description') }}.
-                <a v-if="defs[evalKey]" :href="defs[evalKey].url">{{ $t('details')}}</a>
+                <a v-if="defs[evalKey]" :href="defs[evalKey].url">{{ $t('details') }}</a>
               </i-card>
             </div>
           </span>
@@ -33,29 +33,27 @@
       </tr>
       </thead>
       <tbody>
-        <template v-for="(key, i) in Object.keys(groupedData)" :key="i">
-          <tr v-for="(subject, j) in groupedData[key].subjects" :key="j">
-            <td v-if="j === 0" :rowspan="groupedData[key].subjects.length" class="_vertical-align:top _padding-left:2">
-              <span class="_font-weight:bold">{{ groupedData[key].label }}</span>
-            </td>
-            <td class="_vertical-align:top _padding-left:2">{{ subject.label }}</td>
-            <td
-                v-for="({ name, value }, k) in subject.evaluations"
-                :key="k"
-                class="_text-align:center"
-                :class="(j === groupedData[key].subjects.length - 1) ? '_padding-bottom:5' : ''"
-            >
-              <i-badge
-                  size="lg"
-                  class="metric _cursor:pointer _padding-x:1"
-                  :class="getEvalColor(name, value)">
-                  <template v-if=" name === 'cer'">{{ shortenCER(value) }}</template>
-                  <template v-else-if="name === 'cer_min_max'">{{ shortenCER(value[0]) + '/' + shortenCER(value[1])}}</template>
-                  <template v-else>{{ value }}</template>
-              </i-badge>
-            </td>
-          </tr>
-        </template>
+      <template v-for="(key, i) in Object.keys(groupedData)" :key="i">
+        <tr v-for="(subject, j) in groupedData[key].subjects" :key="j">
+          <td v-if="j === 0" :rowspan="groupedData[key].subjects.length" class="_vertical-align:top _padding-left:2">
+            <span class="_font-weight:bold">{{ groupedData[key].label }}</span>
+          </td>
+          <td class="_vertical-align:top _padding-left:2">{{ subject.label }}</td>
+          <td
+              v-for="({ name, value }, k) in subject.evaluations"
+              :key="k"
+              class="_text-align:center"
+              :class="(j === groupedData[key].subjects.length - 1) ? '_padding-bottom:5' : ''"
+          >
+            <i-badge
+                size="lg"
+                class="metric _cursor:pointer _padding-x:1"
+                :class="getEvalColor(name, value)">
+              {{ createReadableMetricValue(name, value) }}
+            </i-badge>
+          </td>
+        </tr>
+      </template>
       </tbody>
     </i-table>
     <div>{{ $t('no_table_data') }}</div>
@@ -65,8 +63,7 @@
 <script setup>
 import { watch, ref } from "vue";
 import { useI18n } from "vue-i18n";
-import { getEvalColor } from "@/helpers/eval-colors";
-import { shortenCER } from "@/helpers/shorten-cer";
+import { createReadableMetricValue, getEvalColor } from "@/helpers/utils";
 
 const { t } = useI18n();
 const props = defineProps(['data', 'defs']);
@@ -151,12 +148,14 @@ watch(() => props.data, groupByDocuments, { immediate: true });
 
 .def-label {
   position: relative;
+
   &:hover {
     .def-tooltip {
       visibility: visible;
     }
   }
 }
+
 .def-tooltip {
   visibility: hidden;
   position: absolute;
@@ -172,6 +171,6 @@ watch(() => props.data, groupByDocuments, { immediate: true });
 }
 
 th, th span {
-  font-weight:bold;
+  font-weight: bold;
 }
 </style>
diff --git a/src/helpers/api.js b/src/helpers/api.js
index 4b60dda..585f0d8 100644
--- a/src/helpers/api.js
+++ b/src/helpers/api.js
@@ -1,183 +1,4 @@
-const baseUrl = 'https://raw.githubusercontent.com/OCR-D/quiver-back-end/main/data';
-const workflowsJson = [
-    {
-        "@id": "https://github.com/OCR-D/quiver/tree/data/evaluations/wf1-data345-eval1.json",
-        "label": "OCR workflow 1 on workspace 345",
-        "metadata": {
-            "ocr_workflow": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workflows/1.nf",
-                "label": "OCR Workflow 1"
-            },
-            "eval_workflow": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workflows/eval1.nf",
-                "label": "Evaluation Workflow 1"
-            },
-            "gt_workspace": {
-                "@id": "https://gt.ocr-d.de/workspace/789",
-                "label": "GT workspace 789 (19th century fraktur)"
-            },
-            "ocr_workspace": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workspaces/3000.ocrd.zip",
-                "label": "OCR result workspace 3000"
-            },
-            "eval_workspace": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workspaces/345.ocrd.zip",
-                "label": "Evaluation Workspace 345"
-            },
-            "workflow_steps": {
-                "0": "Processor A",
-                "1": "Processor B"
-            },
-            "workflow_model": "Fraktur_GT4HistOCR",
-            "document_metadata": {
-                "fonts": [
-                    "antiqua",
-                    "fraktur"
-                ],
-                "publication_century": "1800-1900",
-                "publication_decade": "1850-1860",
-                "publication_year": 1855,
-                "number_of_pages": 100,
-                "layout": "simple"
-            }
-        },
-        "evaluation": {
-            "document_wide": {
-                "wall_time": 1234,
-                "cer": 0.57,
-                "cer_min_max": [
-                    0.2,
-                    0.57
-                ]
-            },
-            "by_page": [
-                {
-                    "page_id": "PHYS_0001",
-                    "cer": 0.8,
-                    "processing_time": 2.1
-                }
-            ]
-        }
-    },
-    {
-        "@id": "https://github.com/OCR-D/quiver/tree/data/evaluations/wf2-data345-eval1.json",
-        "label": "OCR Workflow 2 on Data 345",
-        "metadata": {
-            "ocr_workflow": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workflows/2.nf",
-                "label": "OCR Workflow 2"
-            },
-            "eval_workflow": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workflows/eval1.nf",
-                "label": "Evaluation Workflow 1"
-            },
-            "gt_workspace": {
-                "@id": "https://gt.ocr-d.de/workspace/789",
-                "label": "GT workspace 789 (19th century fraktur)"
-            },
-            "ocr_workspace": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workspaces/3000.ocrd.zip",
-                "label": "OCR result workspace 3000"
-            },
-            "eval_workspace": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workspaces/345.ocrd.zip",
-                "label": "Evaluation Workspace 345"
-            },
-            "workflow_steps": {
-                "0": "Processor A",
-                "1": "Processor B"
-            },
-            "workflow_model": "Fraktur_GT4HistOCR",
-            "document_metadata": {
-                "fonts": [
-                    "antiqua",
-                    "fraktur"
-                ],
-                "publication_century": "1800-1900",
-                "publication_decade": "1850-1860",
-                "publication_year": 1855,
-                "number_of_pages": 100,
-                "layout": "simple"
-            }
-        },
-        "evaluation": {
-            "document_wide": {
-                "wall_time": 4567,
-                "cer": 0.9,
-                "cer_min_max": [
-                    0.2,
-                    0.99
-                ]
-            },
-            "by_page": [
-                {
-                    "page_id": "PHYS_0001",
-                    "cer": 0.9,
-                    "processing_time": 2.1
-                }
-            ]
-        }
-    },
-    {
-        "@id": "https://github.com/OCR-D/quiver/tree/data/evaluations/wf2-data345-eval1.json",
-        "label": "OCR Workflow 3 on Data 345",
-        "metadata": {
-            "ocr_workflow": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workflows/2.nf",
-                "label": "OCR Workflow 3"
-            },
-            "eval_workflow": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workflows/eval1.nf",
-                "label": "Evaluation Workflow 1"
-            },
-            "gt_workspace": {
-                "@id": "https://gt.ocr-d.de/workspace/123",
-                "label": "GT workspace 123 (16th century fraktur)"
-            },
-            "ocr_workspace": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workspaces/3000.ocrd.zip",
-                "label": "OCR result workspace 3000"
-            },
-            "eval_workspace": {
-                "@id": "https://github.com/OCR-D/quiver/tree/data/workspaces/345.ocrd.zip",
-                "label": "Evaluation Workspace 345"
-            },
-            "workflow_steps": {
-                "0": "Processor A",
-                "1": "Processor B"
-            },
-            "workflow_model": "Fraktur_GT4HistOCR",
-            "document_metadata": {
-                "fonts": [
-                    "antiqua",
-                    "fraktur"
-                ],
-                "publication_century": "1800-1900",
-                "publication_decade": "1850-1860",
-                "publication_year": 1855,
-                "number_of_pages": 100,
-                "layout": "simple"
-            }
-        },
-        "evaluation": {
-            "document_wide": {
-                "wall_time": 8765,
-                "cer": 0.4,
-                "cer_min_max": [
-                    0.2,
-                    0.4
-                ]
-            },
-            "by_page": [
-                {
-                    "page_id": "PHYS_0001",
-                    "cer": 0.4,
-                    "processing_time": 2.1
-                }
-            ]
-        }
-    },
-];
+const baseUrl = 'https://raw.githubusercontent.com/mweidling/quiver-back-end/add-missing-metrics/data';
 
 async function getProjects() {
     return await request(baseUrl + '/repos.json');
diff --git a/src/helpers/eval-colors.js b/src/helpers/eval-colors.js
deleted file mode 100644
index 8d7ef95..0000000
--- a/src/helpers/eval-colors.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { ref } from 'vue';
-
-const evalColors = ref({
-    wall_time: {
-        'eval-positive': 2000,
-        'eval-medium': 4000,
-        'eval-negative': 6000
-    },
-    cer: {
-        'eval-positive': 0.6,
-        'eval-medium': 0.8,
-        'eval-negative': 0.9
-    },
-    cer_min_max: {
-        'eval-positive': 0.6,
-        'eval-medium': 0.8,
-        'eval-negative': 0.9
-    }
-});
-
-const getEvalColor = (name, value) => {
-    const colorMap = evalColors.value[name];
-    if (colorMap) {
-        const keys = Object.keys(colorMap);
-        return keys.find((key, i) => {
-            const isLast = i === keys.length - 1;
-            return value <= colorMap[key] || isLast && value > colorMap[key];
-        });
-    }
-    return null;
-};
-
-const setEvalColors = (data) => {
-    const allCERs = [];
-    const allWallTimes = [];
-
-    data
-      .filter(({ evaluation_results }) => !!(evaluation_results))
-      .forEach(({ evaluation_results }) => {
-        const { document_wide: evals } = evaluation_results;
-
-        if (!evals) return;
-
-        const { cer, wall_time, cer_min_max } = evals;
-
-        if (cer) allCERs.push(cer);
-        if (wall_time) allWallTimes.push(wall_time);
-    });
-
-    const minCER = Math.min(...allCERs);
-    const maxCER = Math.max(...allCERs);
-
-    const minWallTime = Math.min(...allWallTimes);
-    const maxWallTime = Math.max(...allWallTimes);
-
-    const diffCER = maxCER - minCER;
-    const stepCER = diffCER / 3;
-
-    evalColors.value.cer["eval-positive"] = minCER + stepCER;
-    evalColors.value.cer["eval-medium"] = minCER + 2 * stepCER;
-    evalColors.value.cer["eval-negative"] = minCER + 3 * stepCER;
-
-    const diffallTime = maxWallTime - minWallTime;
-    const stepWallTime = diffallTime / 3;
-
-    evalColors.value.wall_time["eval-positive"] = minWallTime + stepWallTime;
-    evalColors.value.wall_time["eval-medium"] = minWallTime + 2 * stepWallTime;
-    evalColors.value.wall_time["eval-negative"] = minWallTime + 3 * stepWallTime;
-};
-
-export {
-    getEvalColor,
-    setEvalColors
-};
diff --git a/src/helpers/shorten-cer.js b/src/helpers/shorten-cer.js
index 2735c55..0c70712 100644
--- a/src/helpers/shorten-cer.js
+++ b/src/helpers/shorten-cer.js
@@ -1,5 +1,11 @@
-const shortenCER = (value) => {
-  return Math.round(value * 1000) / 1000;
+const createReadableMetricValue = (key, value) => {
+  if (['cer_mean', 'cer_median', 'wer', 'pages_per_minute', 'cer_standard_deviation'].includes(key)) {
+    return shortenMetricValue(value);
+  } else if (key === 'cer_range') {
+    return shortenMetricValue(value[0]) + ' / ' + shortenMetricValue(value[1]);
+  }
+
+  return value;
 };
 
 export {
diff --git a/src/helpers/store.js b/src/helpers/store.js
index 4a0d404..6d1f179 100644
--- a/src/helpers/store.js
+++ b/src/helpers/store.js
@@ -1,8 +1,16 @@
 import { reactive } from 'vue';
 
 export const store = reactive({
-    repos: [],
-    setRepos(repos) {
-        this.repos = repos;
-    }
+  repos: [],
+  evaluations: [],
+  metricDefinitions: {},
+  setRepos(repos) {
+    this.repos = repos;
+  },
+  setEvaluations(evaluations) {
+    this.evaluations = evaluations;
+  },
+  setMetricDefinitions(defs) {
+    this.metricDefinitions = defs;
+  }
 });
diff --git a/src/helpers/utils.js b/src/helpers/utils.js
new file mode 100644
index 0000000..c10fd6b
--- /dev/null
+++ b/src/helpers/utils.js
@@ -0,0 +1,73 @@
+import { ref } from 'vue';
+
+const utils = ref({});
+
+const getEvalColor = (name, value) => {
+  const colorMap = utils.value[name];
+  if (colorMap) {
+    const keys = Object.keys(colorMap);
+    return keys.find((key, i) => {
+      const isLast = i === keys.length - 1;
+      return value <= colorMap[key] || isLast && value > colorMap[key];
+    });
+  }
+  return null;
+};
+
+const setEvalColors = (data) => {
+  const allValues = [];
+
+  data
+    .filter(({ evaluation_results }) => !!(evaluation_results))
+    .forEach(({ evaluation_results }) => {
+      const { document_wide: evals } = evaluation_results;
+
+      if (!evals) return;
+
+      Object
+        .keys(evals)
+        .filter(key => !Array.isArray(key))
+        .forEach(key => {
+          allValues[key] = [...(allValues[key] ?? []), evals[key]];
+        });
+    });
+
+  Object
+    .keys(allValues)
+    .forEach(key => {
+      const singleMetricValues = allValues[key];
+      const min = Math.min(...singleMetricValues);
+      const max = Math.max(...singleMetricValues);
+
+      const diff = max - min;
+      const step = diff / 3;
+
+      if (!utils.value[key]) {
+        utils.value[key] = {};
+      }
+
+      utils.value[key]["eval-positive"] = min + step;
+      utils.value[key]["eval-medium"] = min + 2 * step;
+      utils.value[key]["eval-negative"] = min + 3 * step;
+    });
+};
+
+const shortenMetricValue = (value) => {
+  return Math.round(value * 1000) / 1000;
+};
+
+const createReadableMetricValue = (key, value) => {
+  if (['cer_mean', 'cer_median', 'wer', 'pages_per_minute', 'cer_standard_deviation'].includes(key)) {
+    return shortenMetricValue(value);
+  } else if (key === 'cer_range') {
+    return shortenMetricValue(value[0]) + ' / ' + shortenMetricValue(value[1]);
+  }
+
+  return value;
+};
+
+export {
+  getEvalColor,
+  setEvalColors,
+  createReadableMetricValue
+};
diff --git a/src/locales/de.json b/src/locales/de.json
index 2e740eb..0c4bedc 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -32,5 +32,7 @@
   "change": "Ändern",
   "datasets_selected": "Datasets ausgewählt",
   "of": "von",
-  "select_all": "Alle auswählen"
+  "select_all": "Alle auswählen",
+  "document": "Dokument",
+  "workflow": "Workflow"
 }
diff --git a/src/locales/en.json b/src/locales/en.json
index 4bc3ad7..449f4f1 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -26,5 +26,8 @@
   "change": "Change",
   "datasets_selected": "datasets selected",
   "of": "of",
-  "select_all": "Select all"
+  "select_all": "Select all",
+  "document": "Document",
+  "workflow": "Workflow"
+
 }
-- 
GitLab