diff --git a/demo/package-lock.json b/demo/package-lock.json index 0745570f08df7e81b284b2a4809709799408cd4a..d7d99b693777755d6ce19fde5d3483480feca866 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -29,7 +29,7 @@ }, "..": { "name": "@polaris/dashboard-sdk", - "version": "1.0.4", + "version": "1.0.8", "license": "ISC", "dependencies": { "d3": "^7.7.0" diff --git a/demo/src/bar_chart_test.js b/demo/src/bar_chart_test.js new file mode 100644 index 0000000000000000000000000000000000000000..8b82add8390b35b27b88bdf79c1f77cb0ae74a83 --- /dev/null +++ b/demo/src/bar_chart_test.js @@ -0,0 +1,52 @@ +import * as d3 from "d3"; +import { BarChartWidget, ChartJSWidget } from "@polaris/dashboard-sdk"; + +const dict = { + "accessed": 98, + "started": 7, + "graded": 7, + "submitted": 7, + "answered": 38, + "leftUnanswered": 4 +} + +const data = Object.keys(dict).map(key => ({column1: key, column2: dict[key]})); + +export const barChartDef = { "barchart-widget" : + new BarChartWidget( + "A sample bar chart", + "sample description", + data, + { + showLegend: true, + xAxisLabel: "Status", + yAxisLabel: "Count" + }) +} + + +const chartjs_data = {datasets: [{data: Object.keys(dict).map(key => dict[key])}], labels: Object.keys(dict)}; + +export const chartJSBarChartDef = { "chartjs-barchart-widget" : + new ChartJSWidget( + "A sample ChartJS bar chart", + "sample description", + chartjs_data, + { + scales: { + x: { + title: { + display: true, + text: "Status" + } + }, + y: { + title: { + display: true, + text: "Count" + } + } + } + } + ) +} diff --git a/demo/src/grouped_bar_chart_test.js b/demo/src/grouped_bar_chart_test.js index 90b9e010c6127207ff9398260fb734be4c2c6a53..0398dff0ec0526c19fd0ac0ae2c1530b944ff152 100644 --- a/demo/src/grouped_bar_chart_test.js +++ b/demo/src/grouped_bar_chart_test.js @@ -59,4 +59,4 @@ export const simpleGroupedBarChartDef = { "simple-grouped-barchart-widget" : "A sample grouped bar chart", "sample description", dict) -} \ No newline at end of file +} diff --git a/demo/src/heatmap_test.js b/demo/src/heatmap_test.js new file mode 100644 index 0000000000000000000000000000000000000000..6178a0315748ce312d8dbd5303aadb782b243277 --- /dev/null +++ b/demo/src/heatmap_test.js @@ -0,0 +1,117 @@ +import * as d3 from "d3"; +import { HeatMapWidget } from "@polaris/dashboard-sdk"; + +const 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 heatMapDef = { "heatmap-widget" : + new HeatMapWidget( + "A sample heat map", + "sample description", + data, + { + showLegend: false, + xAxisLabel: "Days", + yAxisLabel: "Students" + }) +} diff --git a/demo/src/js/app.js b/demo/src/js/app.js index 14852f40bf325767b644b2eaabf6d3e1f84015d1..d277dec7e081b41bf1c49c97f508287fa40961d5 100644 --- a/demo/src/js/app.js +++ b/demo/src/js/app.js @@ -6,12 +6,15 @@ import { AreaChartWidget, PieChartWidget, StackedBarChartWidget, + ChartJSWidget, + HeatMapWidget } 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"; /** - * JWT Token - hardcoded for demotrastion + * JWT Token - hardcoded for demontrastion * This jwt token is generated in the backend. */ let token = @@ -28,7 +31,7 @@ const subGrid = [ /** * Setup initial widget position and sizes */ -const widgets_confgg = [ +const widgets_config = [ { x: 4, y: 0, @@ -100,6 +103,27 @@ const widgets_confgg = [ h: 6, widgetId: "simple-grouped-barchart-widget", }, + { + x: 0, + y: 13, + w: 5, + h: 5, + widgetId: "chartjs-barchart-widget", + }, + { + x: 5, + y: 13, + w: 5, + h: 5, + widgetId: "barchart-widget", + }, + { + x: 0, + y: 14, + w: 5, + h: 5, + widgetId: "heatmap-widget", + }, ]; /** @@ -235,6 +259,9 @@ const buildWidgets = (data) => { onShowDesc, } ), + ...heatMapDef, + ...barChartDef, + ...chartJSBarChartDef, ...groupedBarChartDef, ...simpleGroupedBarChartDef }; @@ -248,7 +275,7 @@ const setupGrid = (data) => { const widgets = buildWidgets(data); // Initialize grid with widgets at configured positions - grid = initGrid(widgets, widgets_confgg); + grid = initGrid(widgets, widgets_config); // Handle toggle button click const toggleBtn = document.getElementById("toggle-sidebar-btn"); @@ -288,7 +315,8 @@ const onInit = () => { showErrorModal(err.message); }); */ - setupGrid([]) + + setupGrid([]) }; const handleSaveSettingsClick = () => { diff --git a/index.js b/index.js index 128f549ccc503e5d3bd8decb3c5866b065f7f383..42a8cd7b21e8f5a5d58b1a280b8bce2de7780565 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,8 @@ import { AreaChartWidget } from "./src/js/charts/areachart"; import { StackedBarChartWidget } from "./src/js/charts/stacked_barchart"; 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"; export { getResult, @@ -19,5 +21,7 @@ export { StackedBarChartWidget, BaseChartWidget, GroupedBarChartWidget, - SimpleGroupedBarChartWidget + SimpleGroupedBarChartWidget, + ChartJSWidget, + HeatMapWidget }; diff --git a/package-lock.json b/package-lock.json index bdc7d319f864a130693b957949e01cb4c4408093..70af6fd96a8a96f3bf21077a6a0d988578ab99c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "@polaris/dashboard-sdk", - "version": "1.0.4", + "version": "1.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@polaris/dashboard-sdk", - "version": "1.0.4", + "version": "1.0.8", "license": "ISC", "dependencies": { + "chart.js": "^4.3.1", "d3": "^7.7.0" }, "devDependencies": { @@ -1766,6 +1767,11 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -2923,6 +2929,17 @@ "node": ">=4" } }, + "node_modules/chart.js": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.1.tgz", + "integrity": "sha512-QHuISG3hTJ0ftq0I0f5jqH9mNVO9bqG8P+zvMOVslgKajQVvFEX7QAhYNJ+QEmw+uYTwo8XpTimaB82oeTWjxw==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", diff --git a/package.json b/package.json index 3c3f7fb6d91c0be06743de1830c965af8a838833..5e7f77f7a9065cf0ee0153fd41676ce004bed610 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "webpack-dev-server": "^4.11.1" }, "dependencies": { + "chart.js": "^4.3.1", "d3": "^7.7.0" }, "peerDependencies": { diff --git a/src/js/charts/areachart.js b/src/js/charts/areachart.js index 8f2e25b424a724f46cdfe96e348cbd6f90785450..acb3cbad1e286e5b1619526fd9c20b5269c1e183 100644 --- a/src/js/charts/areachart.js +++ b/src/js/charts/areachart.js @@ -8,7 +8,7 @@ export class AreaChartWidget extends BaseChartWidget { super(title, description, data, options); } - plot(divWidth, divHeight) { + plot(divWidth, divHeight, el) { const [width, height] = this.clearAndScaleSvg(divWidth, divHeight); this.drawTitle(); diff --git a/src/js/charts/barchart.js b/src/js/charts/barchart.js index d318a352cc3886db5129069d8d34ff4b19a985eb..14f7b2d58a516b3bc2dde8fd575edb63309e3eb6 100644 --- a/src/js/charts/barchart.js +++ b/src/js/charts/barchart.js @@ -8,7 +8,7 @@ export class BarChartWidget extends BaseChartWidget { super(title, description, data, options); } - plot(divWidth, divHeight) { + plot(divWidth, divHeight, el) { const [width, height] = this.clearAndScaleSvg(divWidth, divHeight); this.drawTitle(); diff --git a/src/js/charts/chartjs.js b/src/js/charts/chartjs.js new file mode 100644 index 0000000000000000000000000000000000000000..aaef34b29f9d9d8a2402bbae1df6bb6f9667fe94 --- /dev/null +++ b/src/js/charts/chartjs.js @@ -0,0 +1,50 @@ +import Chart from 'chart.js/auto'; + +export class ChartJSWidget { + constructor(title, description, data, options = {}, type="bar") { + this.title = title; + this.description = description; + this.data = data; + this.options = options; + + if(document.querySelector(".chart-title") == null) + { + let temp_child = document.createElement("div"); + temp_child.classList.add('chart-title'); + document.body.appendChild(temp_child); + } + let temp_child = document.querySelector(".chart-title"); + let style = window.getComputedStyle(temp_child); + let font_weight = style.getPropertyValue('font-weight'); + let font_size = style.getPropertyValue('font-size'); + let font_family = style.getPropertyValue('font-family'); + let title_object = {title: {display: true, text: this.title, font: {weight: font_weight, size: font_size, family: font_family}}}; + + if("plugins" in this.options) + { + this.options = Object.assign(title_object, this.options.plugins) + } + else { + this.options = Object.assign({plugins: title_object}, this.options) + } + + this.type = type; + this.dataIsValid = Array.isArray(data) ? data.length > 0 : !!data; + } + + + plot(divWidth, divHeight, element) { + this.canvas = document.createElement("canvas"); + this.canvas.id = self.crypto.randomUUID(); + element.appendChild(this.canvas); + + this.chart = new Chart(this.canvas.id, { + type: this.type, + data: this.data, + options: this.options + }); + + return null; + } + +} diff --git a/src/js/charts/grouped_barchart.js b/src/js/charts/grouped_barchart.js index b4c28e2537a8f7143e3bc9594a4d484bb90e2738..91199d9cc9631ba371442a2c032f711fdbaccf23 100644 --- a/src/js/charts/grouped_barchart.js +++ b/src/js/charts/grouped_barchart.js @@ -71,7 +71,7 @@ export class GroupedBarChartWidget extends BaseChartWidget { this.svg = d3.select(this.wrapper).append("svg"); } - plot(divWidth, divHeight) { + plot(divWidth, divHeight, el) { let [w, h] = this.clearAndScaleSvg(divWidth, divHeight); // this.drawTitle(); @@ -193,4 +193,4 @@ export class GroupedBarChartWidget extends BaseChartWidget { } -} \ No newline at end of file +} diff --git a/src/js/charts/heatmap.js b/src/js/charts/heatmap.js new file mode 100644 index 0000000000000000000000000000000000000000..2987b3f3900c17d1ef401d44ae622a406e2d4064 --- /dev/null +++ b/src/js/charts/heatmap.js @@ -0,0 +1,65 @@ +import * as d3 from "d3"; +import { BaseChartWidget } from "./base"; + +export class HeatMapWidget extends BaseChartWidget { + constructor(title, description, data, options) { + if (options.transform) data = (data ?? []).map(options.transform); + + super(title, description, data, options); + } + + plot(divWidth, divHeight, el) { + const [width, height] = this.clearAndScaleSvg(divWidth, divHeight); + this.drawTitle(); + + if (!this.dataIsValid) { + this.showErrorMessage(divWidth, divHeight); + return this.wrapper.innerHTML; + } + const groups = d3.map(this.data, function(d){return d.group;}) + const vars = d3.map(this.data, function(d){return d.variable;}) + + const xScale = d3.scaleBand().range([0, width]).padding(0.1); + const yScale = d3.scaleBand().range([height, 0]).padding(0.1); + + 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(); + + 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)); + + const max = d3.max(this.data, (d) => d.value); + + const colorScale = d3.scaleSequential().interpolator(d3.interpolateInferno).domain([0,max]) + + this.g.selectAll() + .data(this.data, function(d) {return d.group+':'+d.variable;}) + .enter() + .append("rect") + .attr("x", function(d) { return xScale(d.group) }) + .attr("y", function(d) { return yScale(d.variable) }) + .attr("rx", 4) + .attr("ry", 4) + .attr("width", xScale.bandwidth() ) + .attr("height", yScale.bandwidth() ) + .style("fill", function(d) { return colorScale(d.value)} ) + .style("stroke-width", 4) + .style("stroke", "none") + .style("opacity", 0.8); + + return this.wrapper.innerHTML; + } +} diff --git a/src/js/charts/linechart.js b/src/js/charts/linechart.js index f09b82ce607c022ebc63c665783169640eb1666c..c2673448d71946a5ca2d495fcf4d7b094adb3c02 100644 --- a/src/js/charts/linechart.js +++ b/src/js/charts/linechart.js @@ -8,7 +8,7 @@ export class LineChartWidget extends BaseChartWidget { super(title, description, data, options); } - plot(divWidth, divHeight) { + plot(divWidth, divHeight, el) { const [width, height] = this.clearAndScaleSvg(divWidth, divHeight); this.drawTitle(); diff --git a/src/js/charts/piechart.js b/src/js/charts/piechart.js index 6dc850f00a5e3c15ce6ca0e511399e4df515bc47..53693adccc4da308fea34cd2a42568384ae065fb 100644 --- a/src/js/charts/piechart.js +++ b/src/js/charts/piechart.js @@ -8,7 +8,7 @@ export class PieChartWidget extends BaseChartWidget { super(title, description, data, options); } - plot(divWidth, divHeight) { + plot(divWidth, divHeight, el) { const translateX = divWidth / 2, translateY = divHeight / 2; diff --git a/src/js/charts/simple_grouped_barchart.js b/src/js/charts/simple_grouped_barchart.js index 8cc50206db5bc6c9ea6848a5b1d702f347384ee9..071e7c8c8115b86f503a271c62d09953e229ef9b 100644 --- a/src/js/charts/simple_grouped_barchart.js +++ b/src/js/charts/simple_grouped_barchart.js @@ -75,7 +75,7 @@ export class SimpleGroupedBarChartWidget extends BaseChartWidget { this.svg = d3.select(this.wrapper).append("svg"); } - plot(divWidth, divHeight) { + plot(divWidth, divHeight, el) { let [w, h] = this.clearAndScaleSvg(divWidth, divHeight); // this.drawTitle(); @@ -197,4 +197,4 @@ export class SimpleGroupedBarChartWidget extends BaseChartWidget { } -} \ No newline at end of file +} diff --git a/src/js/charts/stacked_barchart.js b/src/js/charts/stacked_barchart.js index d65161319a275d4b678884079aee65fdf1fab2b1..7b16de1f8501ea8696cee4108b1161b67b103ea4 100644 --- a/src/js/charts/stacked_barchart.js +++ b/src/js/charts/stacked_barchart.js @@ -9,7 +9,7 @@ export class StackedBarChartWidget extends BaseChartWidget { this.groups = groups; } - plot(divWidth, divHeight) { + plot(divWidth, divHeight, el) { const [width, height] = this.clearAndScaleSvg(divWidth, divHeight); this.drawTitle(); diff --git a/src/js/utils.js b/src/js/utils.js index 459a474615989bca985094921f915dd0949fb783..fa62915fc650c9658ab9330a2c8e5bfd41f4a035 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -52,7 +52,11 @@ function plot(node, widget) { const content = node.el.querySelector(".grid-stack-item-content"); const width = content.offsetWidth; const height = content.offsetHeight; - content.innerHTML = widget.plot(width, height); + const generated = widget.plot(width, height, content); + if(generated != null) + { + content.innerHTML = widget.plot(width, height); + } } /**