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