From 12f8c20b791553a7283fefd28bd9acd7905ab8c6 Mon Sep 17 00:00:00 2001
From: Lennard Strohmeyer <lennard.strohmeyer@digitallearning.gmbh>
Date: Thu, 27 Jul 2023 12:17:52 +0200
Subject: [PATCH] implemented grid chart with arbitrary subcharts, changed
 default color scheme for heatmap, heatmap improvements

---
 demo/src/grid_test.js     | 345 ++++++++++++++++++++++++++++++++++++++
 demo/src/heatmap_test.js  |   2 +-
 demo/src/js/app.js        |  12 +-
 index.js                  |   4 +-
 package-lock.json         |   3 +-
 package.json              |   3 +-
 src/css/widget-theme.scss |   6 +
 src/js/charts/grid.js     | 191 +++++++++++++++++++++
 src/js/charts/heatmap.js  |  29 ++--
 src/js/utils.js           |   2 +-
 10 files changed, 576 insertions(+), 21 deletions(-)
 create mode 100644 demo/src/grid_test.js
 create mode 100644 src/js/charts/grid.js

diff --git a/demo/src/grid_test.js b/demo/src/grid_test.js
new file mode 100644
index 0000000..0f78674
--- /dev/null
+++ b/demo/src/grid_test.js
@@ -0,0 +1,345 @@
+import { GridWidget } from "@polaris/dashboard-sdk";
+
+const data = [
+    {
+        type: "heatmap",
+        name: "Block 1",
+        options: {
+            showLegend: false,
+        },
+        data:
+        [
+            {
+                group: "A",
+                variable: "v1",
+                value: 12
+            },
+            {
+                group: "A",
+                variable: "v2",
+                value: 1
+            },
+            {
+                group: "A",
+                variable: "v3",
+                value: 26
+            },
+            {
+                group: "A",
+                variable: "v4",
+                value: 86
+            },
+            {
+                group: "A",
+                variable: "v5",
+                value: 10
+            },
+            {
+                group: "B",
+                variable: "v1",
+                value: 82
+            },
+            {
+                group: "B",
+                variable: "v2",
+                value: 65
+            },
+            {
+                group: "B",
+                variable: "v3",
+                value: 51
+            },
+            {
+                group: "B",
+                variable: "v4",
+                value: 2
+            },
+            {
+                group: "B",
+                variable: "v5",
+                value: 15
+            },
+            {
+                group: "C",
+                variable: "v1",
+                value: 23
+            },
+            {
+                group: "C",
+                variable: "v2",
+                value: 35
+            },
+            {
+                group: "C",
+                variable: "v3",
+                value: 45
+            },
+            {
+                group: "C",
+                variable: "v4",
+                value: 36
+            },
+            {
+                group: "C",
+                variable: "v5",
+                value: 19
+            },
+            {
+                group: "D",
+                variable: "v1",
+                value: 89
+            },
+            {
+                group: "D",
+                variable: "v2",
+                value: 64
+            },
+            {
+                group: "D",
+                variable: "v3",
+                value: 4
+            },
+            {
+                group: "D",
+                variable: "v4",
+                value: 39
+            },
+            {
+                group: "D",
+                variable: "v5",
+                value: 75
+            },
+        ]
+    },
+    {
+        type: "heatmap",
+        name: "Block 1",
+        options: {
+            showLegend: false,
+        },
+        data:
+        [
+            {
+                group: "A",
+                variable: "v1",
+                value: 12
+            },
+            {
+                group: "A",
+                variable: "v2",
+                value: 1
+            },
+            {
+                group: "A",
+                variable: "v3",
+                value: 26
+            },
+            {
+                group: "A",
+                variable: "v4",
+                value: 86
+            },
+            {
+                group: "A",
+                variable: "v5",
+                value: 10
+            },
+            {
+                group: "B",
+                variable: "v1",
+                value: 82
+            },
+            {
+                group: "B",
+                variable: "v2",
+                value: 65
+            },
+            {
+                group: "B",
+                variable: "v3",
+                value: 51
+            },
+            {
+                group: "B",
+                variable: "v4",
+                value: 2
+            },
+            {
+                group: "B",
+                variable: "v5",
+                value: 15
+            },
+            {
+                group: "C",
+                variable: "v1",
+                value: 23
+            },
+            {
+                group: "C",
+                variable: "v2",
+                value: 35
+            },
+            {
+                group: "C",
+                variable: "v3",
+                value: 45
+            },
+            {
+                group: "C",
+                variable: "v4",
+                value: 36
+            },
+            {
+                group: "C",
+                variable: "v5",
+                value: 19
+            },
+            {
+                group: "D",
+                variable: "v1",
+                value: 89
+            },
+            {
+                group: "D",
+                variable: "v2",
+                value: 64
+            },
+            {
+                group: "D",
+                variable: "v3",
+                value: 4
+            },
+            {
+                group: "D",
+                variable: "v4",
+                value: 39
+            },
+            {
+                group: "D",
+                variable: "v5",
+                value: 75
+            },
+        ]
+    },
+    {
+        type: "heatmap",
+        name: "Block 1",
+        options: {
+            showLegend: false,
+        },
+        data:
+        [
+            {
+                group: "A",
+                variable: "v1",
+                value: 12
+            },
+            {
+                group: "A",
+                variable: "v2",
+                value: 1
+            },
+            {
+                group: "A",
+                variable: "v3",
+                value: 26
+            },
+            {
+                group: "A",
+                variable: "v4",
+                value: 86
+            },
+            {
+                group: "A",
+                variable: "v5",
+                value: 10
+            },
+            {
+                group: "B",
+                variable: "v1",
+                value: 82
+            },
+            {
+                group: "B",
+                variable: "v2",
+                value: 65
+            },
+            {
+                group: "B",
+                variable: "v3",
+                value: 51
+            },
+            {
+                group: "B",
+                variable: "v4",
+                value: 2
+            },
+            {
+                group: "B",
+                variable: "v5",
+                value: 15
+            },
+            {
+                group: "C",
+                variable: "v1",
+                value: 23
+            },
+            {
+                group: "C",
+                variable: "v2",
+                value: 35
+            },
+            {
+                group: "C",
+                variable: "v3",
+                value: 45
+            },
+            {
+                group: "C",
+                variable: "v4",
+                value: 36
+            },
+            {
+                group: "C",
+                variable: "v5",
+                value: 19
+            },
+            {
+                group: "D",
+                variable: "v1",
+                value: 89
+            },
+            {
+                group: "D",
+                variable: "v2",
+                value: 64
+            },
+            {
+                group: "D",
+                variable: "v3",
+                value: 4
+            },
+            {
+                group: "D",
+                variable: "v4",
+                value: 39
+            },
+            {
+                group: "D",
+                variable: "v5",
+                value: 75
+            },
+        ]
+    }
+]
+
+
+export const gridDef = { "grid-widget" :
+    new GridWidget(
+        "A sample grid widget",
+        "sample description",
+        data,
+        {
+            direction: "row"
+        })
+}
diff --git a/demo/src/heatmap_test.js b/demo/src/heatmap_test.js
index 6178a03..5cf0994 100644
--- a/demo/src/heatmap_test.js
+++ b/demo/src/heatmap_test.js
@@ -1,6 +1,6 @@
-import * as d3 from "d3";
 import { HeatMapWidget } from "@polaris/dashboard-sdk";
 
+
 const data = [
     {
         group: "A",
diff --git a/demo/src/js/app.js b/demo/src/js/app.js
index d277dec..f236d11 100644
--- a/demo/src/js/app.js
+++ b/demo/src/js/app.js
@@ -7,12 +7,14 @@ import {
   PieChartWidget,
   StackedBarChartWidget,
   ChartJSWidget,
-  HeatMapWidget
+  HeatMapWidget,
+  GridWidget
 } from "@polaris/dashboard-sdk/dist/bundle";
 import { CourseRatingChart } from "./custom-charts/courseRatingChart";
 import {groupedBarChartDef, simpleGroupedBarChartDef} from "../grouped_bar_chart_test";
 import { barChartDef, chartJSBarChartDef } from "../bar_chart_test";
 import { heatMapDef } from "../heatmap_test";
+import { gridDef } from "../grid_test";
 /**
  * JWT Token - hardcoded for demontrastion
  * This jwt token is generated in the backend.
@@ -124,6 +126,13 @@ const widgets_config = [
     h: 5,
     widgetId: "heatmap-widget",
   },
+  {
+    x: 0,
+    y: 15,
+    w: 8,
+    h: 8,
+    widgetId: "grid-widget",
+  },
 ];
 
 /**
@@ -259,6 +268,7 @@ const buildWidgets = (data) => {
         onShowDesc,
       }
     ),
+    ...gridDef,
     ...heatMapDef,
     ...barChartDef,
     ...chartJSBarChartDef,
diff --git a/index.js b/index.js
index 42a8cd7..3d9421d 100644
--- a/index.js
+++ b/index.js
@@ -10,6 +10,7 @@ import { GroupedBarChartWidget } from "./src/js/charts/grouped_barchart";
 import { SimpleGroupedBarChartWidget } from "./src/js/charts/simple_grouped_barchart";
 import { ChartJSWidget } from "./src/js/charts/chartjs";
 import { HeatMapWidget } from "./src/js/charts/heatmap";
+import { GridWidget } from "./src/js/charts/grid";
 
 export {
   getResult,
@@ -23,5 +24,6 @@ export {
   GroupedBarChartWidget,
   SimpleGroupedBarChartWidget,
   ChartJSWidget,
-  HeatMapWidget
+  HeatMapWidget,
+  GridWidget
 };
diff --git a/package-lock.json b/package-lock.json
index 70af6fd..c8437f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,8 @@
       "license": "ISC",
       "dependencies": {
         "chart.js": "^4.3.1",
-        "d3": "^7.7.0"
+        "d3": "^7.7.0",
+        "d3-scale-chromatic": "^3.0.0"
       },
       "devDependencies": {
         "@babel/core": "^7.20.5",
diff --git a/package.json b/package.json
index 5e7f77f..f503bef 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,8 @@
   },
   "dependencies": {
     "chart.js": "^4.3.1",
-    "d3": "^7.7.0"
+    "d3": "^7.7.0",
+    "d3-scale-chromatic": "^3.0.0"
   },
   "peerDependencies": {
     "gridstack": "^7.1.1"
diff --git a/src/css/widget-theme.scss b/src/css/widget-theme.scss
index 9318dd5..ea84523 100644
--- a/src/css/widget-theme.scss
+++ b/src/css/widget-theme.scss
@@ -5,6 +5,12 @@
   font-weight: bold;
 }
 
+.chart-subtitle {
+  font-size: 15px;
+  font-weight: bold;
+  text-align: center;
+}
+
 .chart-error-message {
   font-size: 15px;
   font-style: italic;
diff --git a/src/js/charts/grid.js b/src/js/charts/grid.js
new file mode 100644
index 0000000..a26bb10
--- /dev/null
+++ b/src/js/charts/grid.js
@@ -0,0 +1,191 @@
+import * as d3 from "d3";
+import { BaseChartWidget } from "./base";
+
+import { AreaChartWidget } from "./areachart";
+import { BarChartWidget } from "./barchart";
+import { ChartJSWidget } from "./chartjs";
+import { GroupedBarChartWidget } from "./grouped_barchart";
+import { HeatMapWidget } from "./heatmap";
+import { LineChartWidget } from "./linechart";
+import { PieChartWidget } from "./piechart";
+import { SimpleGroupedBarChartWidget } from "./simple_grouped_barchart";
+import { StackedBarChartWidget } from "./stacked_barchart";
+
+const chart_types = [
+  "areachart",
+  "barchart",
+  "chartjs",
+  "grouped_barchart",
+  "heatmap",
+  "linechart",
+  "piechart",
+  "simple_grouped_barchart",
+  "stacked_barchart"
+]
+
+
+/*
+ * // config: row oder column
+  [
+      {type: "", name: "", options: {}, data: []},
+      ...
+  ]
+ *
+ *
+ */
+
+export class GridWidget extends BaseChartWidget {
+  constructor(title, description, data, options) {
+    if (options.transform) data = (data ?? []).map(options.transform);
+
+    super(title, description, data, options);
+
+    this.svg.remove();
+    delete this.svg;
+    delete this.wrapper;
+  }
+
+  plot(divWidth, divHeight, el) {
+    //const [width, height] = this.clearAndScaleSvg(divWidth, divHeight);
+    //this.drawTitle(); // TODO: override!
+
+    if (!this.dataIsValid) {
+      this.showErrorMessage(divWidth, divHeight);
+      return this.wrapper.innerHTML;
+    }
+
+    const title_element = document.createElement("div");
+    title_element.className = "chart-title";
+    title_element.innerHTML = this.title;
+    el.appendChild(title_element);
+
+    let width = divWidth;
+    let height = divHeight;
+
+    const widget = document.createElement("div");
+    widget.style.display = "flex";
+    if (this.options.direction) {
+      widget.style.flexDirection = this.options.direction; // row, column
+    }
+    else {
+      widget.style.flexDirection = "row"; // default
+    }
+    if(widget.style.flexDirection == "row") {
+      width /= this.data.length;
+    } else {
+      height /= this.data.length;
+    }
+
+    let temp_subtitle = document.createElement("div");
+    temp_subtitle.className = "chart-subtitle";
+    temp_subtitle.innerHTML = "temp";
+    el.appendChild(temp_subtitle);
+    height -= title_element.offsetHeight;
+    height -= temp_subtitle.offsetHeight;
+    el.removeChild(temp_subtitle);
+
+    el.appendChild(widget);
+
+    for(const chart of this.data)
+    {
+      let subwidget_div = document.createElement("div");
+      widget.appendChild(subwidget_div);
+
+      let subwidget = null;
+      switch(chart.type)
+      {
+        case "areachart":
+          subwidget = new AreaChartWidget(
+            "",
+            "",
+            chart.data,
+            chart.options
+          );
+          break;
+        case "barchart":
+          subwidget = new BarChartWidget(
+            "",
+            "",
+            chart.data,
+            chart.options
+          );
+          break;
+        case "chartjs":
+          subwidget = new ChartJSWidget(
+            "",
+            "",
+            chart.data,
+            chart.options
+          );
+          break;
+        case "grouped_barchart":
+          subwidget = new GroupedBarChartWidget(
+            "",
+            "",
+            chart.data,
+            chart.options
+          );
+          break;
+        case "heatmap":
+          subwidget = new HeatMapWidget(
+            "",
+            "",
+            chart.data,
+            chart.options
+          );
+          break;
+        case "linechart":
+          subwidget = new LineChartWidget(
+            "",
+            "",
+            chart.data,
+            chart.options
+          );
+          break;
+        case "piechart":
+          subwidget = new PieChartWidget(
+            "",
+            "",
+            chart.data,
+            chart.options
+          );
+          break;
+        case "simple_grouped_barchart":
+          subwidget = new SimpleGroupedBarChartWidget(
+            "",
+            "",
+            chart.data,
+            chart.options
+          );
+          break;
+        case "stacked_barchart":
+          subwidget = new StackedBarChartWidget(
+            "",
+            "",
+            chart.data,
+            chart.options
+          );
+          break;
+        default: // unknown -> render nothing
+          this.widget.removeChild(subwidget_div);
+          continue;
+      }
+
+      const generated = subwidget.plot(width, height, subwidget_div);
+      if(generated != null)
+      {
+        subwidget_div.innerHTML = generated;
+      }
+
+      subwidget_div.querySelector('.chart-title')?.parentElement.remove();
+      subwidget_div.querySelector('.question-mark')?.parentElement.remove();
+      // TODO paint axes yes / no ?
+
+      const subwidget_title = document.createElement("div");
+      subwidget_title.className = "chart-subtitle";
+      subwidget_title.innerHTML = chart.name;
+      subwidget_div.appendChild(subwidget_title);
+    }
+    return null; // we write directly to the DOM, thus return null
+  }
+}
diff --git a/src/js/charts/heatmap.js b/src/js/charts/heatmap.js
index 2987b3f..9fc714a 100644
--- a/src/js/charts/heatmap.js
+++ b/src/js/charts/heatmap.js
@@ -1,5 +1,6 @@
 import * as d3 from "d3";
 import { BaseChartWidget } from "./base";
+//import { interpolateGreens } from "d3-scale-chromatic";
 
 export class HeatMapWidget extends BaseChartWidget {
   constructor(title, description, data, options) {
@@ -25,25 +26,23 @@ export class HeatMapWidget extends BaseChartWidget {
     xScale.domain(groups);
     yScale.domain(vars);
 
-    this.g.append("g")
-    .style("font-size", 15)
-    .attr("transform", "translate(0," + height + ")")
-    .call(d3.axisBottom(xScale).tickSize(0))
-    .select(".domain").remove();
+    if (this.options.showLegend)
+    {
+      this.g.append("g")
+      .style("font-size", 15)
+      .attr("transform", "translate(0," + height + ")")
+      .call(d3.axisBottom(xScale).tickSize(0))
+      .select(".domain").remove();
 
-    this.g.append("g")
-    .style("font-size", 15)
-    .call(d3.axisLeft(yScale).tickSize(0))
-    .select(".domain").remove()
-    //this.appendXAxis(xScale, height);
-    //this.appendXAxisLabel(width, height);
-    //this.appendYAxisLabel();
-
-    //this.g.append("g").attr("transform", "translate(0, 0)").call(d3.axisLeft(yScale));
+      this.g.append("g")
+      .style("font-size", 15)
+      .call(d3.axisLeft(yScale).tickSize(0))
+      .select(".domain").remove()
+    }
 
     const max = d3.max(this.data, (d) => d.value);
 
-    const colorScale = d3.scaleSequential().interpolator(d3.interpolateInferno).domain([0,max])
+    const colorScale = d3.scaleSequential().interpolator(d3.interpolateGreens).domain([0,max])
 
     this.g.selectAll()
     .data(this.data, function(d) {return d.group+':'+d.variable;})
diff --git a/src/js/utils.js b/src/js/utils.js
index fa62915..41c1406 100644
--- a/src/js/utils.js
+++ b/src/js/utils.js
@@ -55,7 +55,7 @@ function plot(node, widget) {
   const generated = widget.plot(width, height, content);
   if(generated != null)
   {
-    content.innerHTML = widget.plot(width, height);
+    content.innerHTML = generated;
   }
 }
 
-- 
GitLab