From f9b3af44af6897b286de0674bc9a919c689ff64e Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Wed, 20 Sep 2023 09:36:20 +0200 Subject: [PATCH] Working JavaScript charts --- js/package-lock.json | 15 +++++ js/package.json | 1 + js/src/chart/BarChart.js | 27 ++++++++ js/src/chart/ChartJsPart.js | 65 +++++++++++++++++++ js/src/chart/ChartPart.js | 34 ++++++++++ js/src/{graph => chart}/TestGraph.js | 0 js/src/{graph => chart}/export-package.js | 8 ++- js/src/{graph => chart}/index.html | 1 - js/src/{graph => chart}/index.js | 0 js/webpack.common.js | 10 +-- .../org/argeo/app/geo/swt/MapUiProvider.java | 2 +- .../{SwtJSMapPart.java => SwtJsMapPart.java} | 4 +- .../argeo/app/swt/chart/AbstractJsChart.java | 27 ++++++++ .../argeo/app/swt/chart/SwtJsBarChart.java | 61 +++++++++++++++++ .../argeo/app/swt/js/SwtBrowserJsPart.java | 39 ++++++++++- 15 files changed, 281 insertions(+), 13 deletions(-) create mode 100644 js/src/chart/BarChart.js create mode 100644 js/src/chart/ChartJsPart.js create mode 100644 js/src/chart/ChartPart.js rename js/src/{graph => chart}/TestGraph.js (100%) rename js/src/{graph => chart}/export-package.js (66%) rename js/src/{graph => chart}/index.html (69%) rename js/src/{graph => chart}/index.js (100%) rename swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/{SwtJSMapPart.java => SwtJsMapPart.java} (95%) create mode 100644 swt/org.argeo.app.swt/src/org/argeo/app/swt/chart/AbstractJsChart.java create mode 100644 swt/org.argeo.app.swt/src/org/argeo/app/swt/chart/SwtJsBarChart.java diff --git a/js/package-lock.json b/js/package-lock.json index ec19936..7e38ea8 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@nieuwlandgeo/sldreader": "0.3.x", "chart.js": "4.x.x", + "chartjs-plugin-annotation": "^3.0.1", "ol": "8.x.x" }, "devDependencies": { @@ -1346,6 +1347,14 @@ "pnpm": ">=7" } }, + "node_modules/chartjs-plugin-annotation": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.0.1.tgz", + "integrity": "sha512-hlIrXXKqSDgb+ZjVYHefmlZUXK8KbkCPiynSVrTb/HjTMkT62cOInaT1NTQCKtxKKOm9oHp958DY3RTAFKtkHg==", + "peerDependencies": { + "chart.js": ">=4.0.0" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -7889,6 +7898,12 @@ "@kurkle/color": "^0.3.0" } }, + "chartjs-plugin-annotation": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.0.1.tgz", + "integrity": "sha512-hlIrXXKqSDgb+ZjVYHefmlZUXK8KbkCPiynSVrTb/HjTMkT62cOInaT1NTQCKtxKKOm9oHp958DY3RTAFKtkHg==", + "requires": {} + }, "chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", diff --git a/js/package.json b/js/package.json index 08a3865..4cef5e9 100644 --- a/js/package.json +++ b/js/package.json @@ -25,6 +25,7 @@ "dependencies": { "@nieuwlandgeo/sldreader": "0.3.x", "chart.js": "4.x.x", + "chartjs-plugin-annotation": "^3.0.1", "ol": "8.x.x" } } diff --git a/js/src/chart/BarChart.js b/js/src/chart/BarChart.js new file mode 100644 index 0000000..d65b9cc --- /dev/null +++ b/js/src/chart/BarChart.js @@ -0,0 +1,27 @@ +import Chart from 'chart.js/auto'; + +import ChartJsPart from './ChartJsPart.js'; + +export default class BarChart extends ChartJsPart { + /** Constructor taking the mapName as an argument. */ + constructor(chartName) { + super(chartName); + this.setChart(new Chart(this.getChartCanvas(), { + type: 'bar', + data: { + datasets: [] + }, + options: { + scales: { + y: { + beginAtZero: true + }, + }, + animation: false, + } + })); + + } + + +} diff --git a/js/src/chart/ChartJsPart.js b/js/src/chart/ChartJsPart.js new file mode 100644 index 0000000..ac60ce3 --- /dev/null +++ b/js/src/chart/ChartJsPart.js @@ -0,0 +1,65 @@ +import ChartPart from './ChartPart.js'; + +import { Chart } from 'chart.js'; +import annotationPlugin from 'chartjs-plugin-annotation'; + +Chart.register(annotationPlugin); + +export default class ChartJsPart extends ChartPart { + #chart; + + /** Constructor taking the mapName as an argument. */ + constructor(chartName) { + super(chartName); + } + + setChart(chart) { + this.#chart = chart; + } + + getChart() { + return this.#chart; + } + + // + // DATA + // + setLabels(labels) { + const chart = this.getChart(); + chart.data.labels = labels; + this.update(); + } + + addDataset(label, data) { + const chart = this.getChart(); + chart.data.datasets.push({ + label: label, + data: data, + borderWidth: 1 + }); + this.update(); + } + + setData(labels, label, data) { + this.clearDatasets(); + this.setLabels(labels); + this.addDataset(label, data); + } + + setDatasets(labels, datasets) { + const chart = this.getChart(); + chart.data.datasets = datasets; + chart.data.labels = labels; + this.update(); + } + + clearDatasets() { + const chart = this.getChart(); + chart.data.datasets = []; + this.update(); + } + + update() { + this.#chart.update(); + } +} diff --git a/js/src/chart/ChartPart.js b/js/src/chart/ChartPart.js new file mode 100644 index 0000000..1fe9221 --- /dev/null +++ b/js/src/chart/ChartPart.js @@ -0,0 +1,34 @@ +/** API to be used by Java. + * @module MapPart + */ + +/** Abstract base class for displaying a map. */ +export default class ChartPart { + + /** The name of the chart, will also be the name of the variable */ + #chartName; + + constructor(chartName) { + this.#chartName = chartName; + this.createChartCanvas(this.#chartName); + } + + + // + // HTML + // + /** Create the div element where the chart will be displayed. */ + createChartCanvas(id) { + const chartDiv = document.createElement('canvas'); + chartDiv.id = id; + //chartDiv.style.cssText = 'width: 100%;'; + chartDiv.style.cssText = 'width: 100%; height: 100vh;'; + document.body.appendChild(chartDiv); + } + + /** Get the div element where the chart is displayed. */ + getChartCanvas() { + return document.getElementById(this.#chartName); + } + +} \ No newline at end of file diff --git a/js/src/graph/TestGraph.js b/js/src/chart/TestGraph.js similarity index 100% rename from js/src/graph/TestGraph.js rename to js/src/chart/TestGraph.js diff --git a/js/src/graph/export-package.js b/js/src/chart/export-package.js similarity index 66% rename from js/src/graph/export-package.js rename to js/src/chart/export-package.js index ec8b4ee..5fbcc0a 100644 --- a/js/src/graph/export-package.js +++ b/js/src/chart/export-package.js @@ -1,3 +1,4 @@ +import BarChart from './BarChart.js'; import TestGraph from './TestGraph.js'; //import { rectY, binX } from "@observablehq/plot"; @@ -6,11 +7,12 @@ if (typeof globalThis.argeo === 'undefined') globalThis.argeo = {}; if (typeof globalThis.argeo.app === 'undefined') globalThis.argeo.app = {}; -if (typeof globalThis.argeo.app.graph === 'undefined') - globalThis.argeo.app.graph = {}; +if (typeof globalThis.argeo.app.chart === 'undefined') + globalThis.argeo.app.chart = {}; // PUBLIC CLASSES -globalThis.argeo.app.graph.TestGraph = TestGraph; +globalThis.argeo.app.chart.BarChart = BarChart; +globalThis.argeo.app.chart.TestGraph = TestGraph; //const plot = rectY({ length: 10000 }, binX({ y: "count" }, { x: Math.random })).plot(); //const div = document.querySelector("#myplot"); diff --git a/js/src/graph/index.html b/js/src/chart/index.html similarity index 69% rename from js/src/graph/index.html rename to js/src/chart/index.html index b9115a8..72f6094 100644 --- a/js/src/graph/index.html +++ b/js/src/chart/index.html @@ -6,7 +6,6 @@ - \ No newline at end of file diff --git a/js/src/graph/index.js b/js/src/chart/index.js similarity index 100% rename from js/src/graph/index.js rename to js/src/chart/index.js diff --git a/js/webpack.common.js b/js/webpack.common.js index dccac82..ba57965 100644 --- a/js/webpack.common.js +++ b/js/webpack.common.js @@ -6,7 +6,7 @@ const path = require('path'); module.exports = { entry: { "geo": './src/geo/index.js', - "graph": './src/graph/index.js', + "chart": './src/chart/index.js', }, output: { filename: '[name].[contenthash].js', @@ -50,11 +50,11 @@ module.exports = { chunks: ['geo'], }), new HtmlWebpackPlugin({ - title: 'Argeo Suite Graph JS', - template: 'src/graph/index.html', + title: 'Argeo Suite Chart JS', + template: 'src/chart/index.html', scriptLoading: 'module', - filename: 'graph.html', - chunks: ['graph'], + filename: 'chart.html', + chunks: ['chart'], }), ], diff --git a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java index fa70146..b758923 100644 --- a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java +++ b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/MapUiProvider.java @@ -10,7 +10,7 @@ public class MapUiProvider implements SwtUiProvider { @Override public Control createUiPart(Composite parent, Content context) { - SwtJSMapPart map = new SwtJSMapPart("defaultOverviewMap", parent, 0); + SwtJsMapPart map = new SwtJsMapPart("defaultOverviewMap", parent, 0); map.setCenter(13.404954, 52.520008); // Berlin // map.setCenter(-74.00597, 40.71427); // NYC // map.addPoint(-74.00597, 40.71427, null); diff --git a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJsMapPart.java similarity index 95% rename from swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java rename to swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJsMapPart.java index ad1a9fa..5e90d39 100644 --- a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java +++ b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJsMapPart.java @@ -12,13 +12,13 @@ import org.eclipse.swt.widgets.Composite; /** * An SWT implementation of {@link MapPart} based on JavaScript. */ -public class SwtJSMapPart extends SwtBrowserJsPart implements MapPart { +public class SwtJsMapPart extends SwtBrowserJsPart implements MapPart { static final long serialVersionUID = 2713128477504858552L; private String jsImplementation = JsImplementation.OPENLAYERS_MAP_PART.getJsClass(); private final String mapName;// = "argeoMap"; - public SwtJSMapPart(String mapName, Composite parent, int style) { + public SwtJsMapPart(String mapName, Composite parent, int style) { super(parent, style, "/pkg/org.argeo.app.js/geo.html"); this.mapName = mapName; } diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/chart/AbstractJsChart.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/chart/AbstractJsChart.java new file mode 100644 index 0000000..f114621 --- /dev/null +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/chart/AbstractJsChart.java @@ -0,0 +1,27 @@ +package org.argeo.app.swt.chart; + +import org.argeo.app.swt.js.SwtBrowserJsPart; +import org.eclipse.swt.widgets.Composite; + +/** Base class for charts. */ +public abstract class AbstractJsChart extends SwtBrowserJsPart { + private String chartName; + + protected abstract String getJsImplementation(); + + public AbstractJsChart(String chartName, Composite parent, int style) { + super(parent, style, "/pkg/org.argeo.app.js/chart.html"); + this.chartName = chartName; + } + + @Override + protected void init() { + // create chart + doExecute(getJsChartVar() + " = new " + getJsImplementation() + "('" + chartName + "');"); + } + + protected String getJsChartVar() { + return getJsVarName(chartName); + } + +} diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/chart/SwtJsBarChart.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/chart/SwtJsBarChart.java new file mode 100644 index 0000000..332d0eb --- /dev/null +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/chart/SwtJsBarChart.java @@ -0,0 +1,61 @@ +package org.argeo.app.swt.chart; + +import java.io.StringWriter; + +import org.eclipse.swt.widgets.Composite; + +import jakarta.json.Json; +import jakarta.json.stream.JsonGenerator; + +public class SwtJsBarChart extends AbstractJsChart { + + public SwtJsBarChart(String chartName, Composite parent, int style) { + super(chartName, parent, style); + } + + @Override + protected String getJsImplementation() { + return "globalThis.argeo.app.chart.BarChart"; + } + + public void setLabels(String[] labels) { + callMethod(getJsChartVar(), "setLabels(%s)", toJsArray(labels)); + } + + public void addDataset(String label, int[] values) { + callMethod(getJsChartVar(), "addDataset('%s', %s)", label, toJsArray(values)); + } + + public void setData(String[] labels, String label, int[] values) { + callMethod(getJsChartVar(), "setData(%s, '%s', %s)", toJsArray(labels), label, toJsArray(values)); + } + + public void setDatasets(String[] labels, String[] label, int[][] values) { + callMethod(getJsChartVar(), "setDatasets(%s, %s)", toJsArray(labels), toDatasets(label, values)); + } + + protected String toDatasets(String[] label, int[][] values) { + if (label.length != values.length) + throw new IllegalArgumentException("Arrays must have the same length"); + StringWriter writer = new StringWriter(); + JsonGenerator g = Json.createGenerator(writer); + g.writeStartArray(); + for (int i = 0; i < label.length; i++) { + g.writeStartObject(); + g.write("label", label[i]); + g.writeStartArray("data"); + for (int j = 0; j < values[i].length; j++) { + g.write(values[i][j]); + } + g.writeEnd();// data array + g.writeEnd();// dataset + } + g.writeEnd(); + g.close(); + return writer.toString(); + } + + public void clearDatasets() { + callMethod(getJsChartVar(), "clearDatasets()"); + } +} diff --git a/swt/org.argeo.app.swt/src/org/argeo/app/swt/js/SwtBrowserJsPart.java b/swt/org.argeo.app.swt/src/org/argeo/app/swt/js/SwtBrowserJsPart.java index 8ae34d5..fac099f 100644 --- a/swt/org.argeo.app.swt/src/org/argeo/app/swt/js/SwtBrowserJsPart.java +++ b/swt/org.argeo.app.swt/src/org/argeo/app/swt/js/SwtBrowserJsPart.java @@ -1,6 +1,8 @@ package org.argeo.app.swt.js; +import java.util.Arrays; import java.util.Locale; +import java.util.StringJoiner; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Function; @@ -135,10 +137,45 @@ public class SwtBrowserJsPart { return GLOBAL_THIS_ + name; } + protected static String toJsArray(int... arr) { + return Arrays.toString(arr); + } + + protected static String toJsArray(long... arr) { + return Arrays.toString(arr); + } + + protected static String toJsArray(double... arr) { + return Arrays.toString(arr); + } + + protected static String toJsArray(String... arr) { + return toJsArray((Object[]) arr); + } + + protected static String toJsArray(Object... arr) { + StringJoiner sj = new StringJoiner(",", "[", "]"); + for (Object o : arr) { + sj.add(toJsValue(o)); + } + return sj.toString(); + } + + protected static String toJsValue(Object o) { + if (o instanceof CharSequence) + return '\"' + o.toString() + '\"'; + else if (o instanceof Number) + return o.toString(); + else if (o instanceof Boolean) + return o.toString(); + else + return '\"' + o.toString() + '\"'; + } + /* * ACCESSORS */ - + public Control getControl() { return browser; } -- 2.30.2