From: Mathieu Baudier Date: Wed, 20 Sep 2023 07:36:20 +0000 (+0200) Subject: Working JavaScript charts X-Git-Tag: v2.3.16~25 X-Git-Url: https://git.argeo.org/?p=gpl%2Fargeo-suite.git;a=commitdiff_plain;h=f9b3af44af6897b286de0674bc9a919c689ff64e Working JavaScript charts --- 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/chart/TestGraph.js b/js/src/chart/TestGraph.js new file mode 100644 index 0000000..9cc67db --- /dev/null +++ b/js/src/chart/TestGraph.js @@ -0,0 +1,27 @@ +import Chart from 'chart.js/auto'; + +export default class TestGraph { + + init() { + const ctx = document.getElementById('myChart'); + + new Chart(ctx, { + type: 'bar', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + borderWidth: 1 + }] + }, + options: { + scales: { + y: { + beginAtZero: true + } + } + } + }); + } +} diff --git a/js/src/chart/export-package.js b/js/src/chart/export-package.js new file mode 100644 index 0000000..5fbcc0a --- /dev/null +++ b/js/src/chart/export-package.js @@ -0,0 +1,21 @@ +import BarChart from './BarChart.js'; +import TestGraph from './TestGraph.js'; +//import { rectY, binX } from "@observablehq/plot"; + +// PSEUDO PACKAGE +if (typeof globalThis.argeo === 'undefined') + globalThis.argeo = {}; +if (typeof globalThis.argeo.app === 'undefined') + globalThis.argeo.app = {}; +if (typeof globalThis.argeo.app.chart === 'undefined') + globalThis.argeo.app.chart = {}; + +// PUBLIC CLASSES +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"); +//div.append(plot); + +"use strict"; diff --git a/js/src/chart/index.html b/js/src/chart/index.html new file mode 100644 index 0000000..72f6094 --- /dev/null +++ b/js/src/chart/index.html @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/js/src/chart/index.js b/js/src/chart/index.js new file mode 100644 index 0000000..6ff32b0 --- /dev/null +++ b/js/src/chart/index.js @@ -0,0 +1 @@ +import './export-package.js'; diff --git a/js/src/graph/TestGraph.js b/js/src/graph/TestGraph.js deleted file mode 100644 index 9cc67db..0000000 --- a/js/src/graph/TestGraph.js +++ /dev/null @@ -1,27 +0,0 @@ -import Chart from 'chart.js/auto'; - -export default class TestGraph { - - init() { - const ctx = document.getElementById('myChart'); - - new Chart(ctx, { - type: 'bar', - data: { - labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], - datasets: [{ - label: '# of Votes', - data: [12, 19, 3, 5, 2, 3], - borderWidth: 1 - }] - }, - options: { - scales: { - y: { - beginAtZero: true - } - } - } - }); - } -} diff --git a/js/src/graph/export-package.js b/js/src/graph/export-package.js deleted file mode 100644 index ec8b4ee..0000000 --- a/js/src/graph/export-package.js +++ /dev/null @@ -1,19 +0,0 @@ -import TestGraph from './TestGraph.js'; -//import { rectY, binX } from "@observablehq/plot"; - -// PSEUDO PACKAGE -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 = {}; - -// PUBLIC CLASSES -globalThis.argeo.app.graph.TestGraph = TestGraph; - -//const plot = rectY({ length: 10000 }, binX({ y: "count" }, { x: Math.random })).plot(); -//const div = document.querySelector("#myplot"); -//div.append(plot); - -"use strict"; diff --git a/js/src/graph/index.html b/js/src/graph/index.html deleted file mode 100644 index b9115a8..0000000 --- a/js/src/graph/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/js/src/graph/index.js b/js/src/graph/index.js deleted file mode 100644 index 6ff32b0..0000000 --- a/js/src/graph/index.js +++ /dev/null @@ -1 +0,0 @@ -import './export-package.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 deleted file mode 100644 index ad1a9fa..0000000 --- a/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJSMapPart.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.argeo.app.geo.swt; - -import java.util.concurrent.CompletionStage; -import java.util.function.Consumer; -import java.util.function.Function; -import org.argeo.app.geo.GeoUtils; -import org.argeo.app.geo.ux.JsImplementation; -import org.argeo.app.geo.ux.MapPart; -import org.argeo.app.swt.js.SwtBrowserJsPart; -import org.eclipse.swt.widgets.Composite; - -/** - * An SWT implementation of {@link MapPart} based on JavaScript. - */ -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) { - super(parent, style, "/pkg/org.argeo.app.js/geo.html"); - this.mapName = mapName; - } - - @Override - protected void init() { - // create map - doExecute(getJsMapVar() + " = new " + jsImplementation + "('" + mapName + "');"); - } - - /* - * MapPart.js METHODS - */ - - @Override - public void addPoint(double lng, double lat, String style) { - callMapMethod("addPoint(%f, %f, %s)", lng, lat, style == null ? "'default'" : style); - } - - @Override - public void addUrlLayer(String url, GeoFormat format, String style) { - callMapMethod("addUrlLayer('%s', '%s', %s, false)", url, format.name(), style); - } - - public void addCssUrlLayer(String url, GeoFormat format, String css) { - String style = GeoUtils.createSldFromCss("layer", "Layer", css); - callMapMethod("addUrlLayer('%s', '%s', '%s', true)", url, format.name(), style); - } - - @Override - public void setZoom(int zoom) { - callMapMethod("setZoom(%d)", zoom); - } - - @Override - public void setCenter(double lng, double lat) { - callMapMethod("setCenter(%f, %f)", lng, lat); - } - - protected CompletionStage callMapMethod(String methodCall, Object... args) { - return callMethod(getJsMapVar(), methodCall, args); - } - - private String getJsMapVar() { - return getJsVarName(mapName); - } - - /* - * CALLBACKS - */ - public void onFeatureSelected(Consumer toDo) { - addCallback("FeatureSelected", (arr) -> { - toDo.accept(new FeatureSelectedEvent((String) arr[0])); - return null; - }); - } - - public void onFeatureSingleClick(Consumer toDo) { - addCallback("FeatureSingleClick", (arr) -> { - toDo.accept(new FeatureSingleClickEvent((String) arr[0])); - return null; - }); - } - - public void onFeaturePopup(Function toDo) { - addCallback("FeaturePopup", (arr) -> { - return toDo.apply(new FeaturePopupEvent((String) arr[0])); - }); - } - - protected void addCallback(String suffix, Function toDo) { - getReadyStage().thenAccept((ready) -> { - String functionName = createJsFunction(mapName + "__on" + suffix, toDo); - doExecute(getJsMapVar() + ".callbacks['on" + suffix + "']=" + functionName + ";"); - callMethod(mapName, "enable" + suffix + "()"); - }); - } -} 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 new file mode 100644 index 0000000..5e90d39 --- /dev/null +++ b/swt/org.argeo.app.geo.swt/src/org/argeo/app/geo/swt/SwtJsMapPart.java @@ -0,0 +1,99 @@ +package org.argeo.app.geo.swt; + +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Function; +import org.argeo.app.geo.GeoUtils; +import org.argeo.app.geo.ux.JsImplementation; +import org.argeo.app.geo.ux.MapPart; +import org.argeo.app.swt.js.SwtBrowserJsPart; +import org.eclipse.swt.widgets.Composite; + +/** + * An SWT implementation of {@link MapPart} based on JavaScript. + */ +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) { + super(parent, style, "/pkg/org.argeo.app.js/geo.html"); + this.mapName = mapName; + } + + @Override + protected void init() { + // create map + doExecute(getJsMapVar() + " = new " + jsImplementation + "('" + mapName + "');"); + } + + /* + * MapPart.js METHODS + */ + + @Override + public void addPoint(double lng, double lat, String style) { + callMapMethod("addPoint(%f, %f, %s)", lng, lat, style == null ? "'default'" : style); + } + + @Override + public void addUrlLayer(String url, GeoFormat format, String style) { + callMapMethod("addUrlLayer('%s', '%s', %s, false)", url, format.name(), style); + } + + public void addCssUrlLayer(String url, GeoFormat format, String css) { + String style = GeoUtils.createSldFromCss("layer", "Layer", css); + callMapMethod("addUrlLayer('%s', '%s', '%s', true)", url, format.name(), style); + } + + @Override + public void setZoom(int zoom) { + callMapMethod("setZoom(%d)", zoom); + } + + @Override + public void setCenter(double lng, double lat) { + callMapMethod("setCenter(%f, %f)", lng, lat); + } + + protected CompletionStage callMapMethod(String methodCall, Object... args) { + return callMethod(getJsMapVar(), methodCall, args); + } + + private String getJsMapVar() { + return getJsVarName(mapName); + } + + /* + * CALLBACKS + */ + public void onFeatureSelected(Consumer toDo) { + addCallback("FeatureSelected", (arr) -> { + toDo.accept(new FeatureSelectedEvent((String) arr[0])); + return null; + }); + } + + public void onFeatureSingleClick(Consumer toDo) { + addCallback("FeatureSingleClick", (arr) -> { + toDo.accept(new FeatureSingleClickEvent((String) arr[0])); + return null; + }); + } + + public void onFeaturePopup(Function toDo) { + addCallback("FeaturePopup", (arr) -> { + return toDo.apply(new FeaturePopupEvent((String) arr[0])); + }); + } + + protected void addCallback(String suffix, Function toDo) { + getReadyStage().thenAccept((ready) -> { + String functionName = createJsFunction(mapName + "__on" + suffix, toDo); + doExecute(getJsMapVar() + ".callbacks['on" + suffix + "']=" + functionName + ";"); + callMethod(mapName, "enable" + suffix + "()"); + }); + } +} 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; }