From c1f30dbde15435258e642ad11d5c6c1132376ffa Mon Sep 17 00:00:00 2001 From: TheNoumanDev Date: Thu, 31 Oct 2024 15:18:48 +0500 Subject: [PATCH] updated chartJS implementation for web and native --- modules/ensemble/lib/util/chart_utils.dart | 78 +++++++++++++++++ .../{ => chart_js}/chart_js.dart | 83 ++++++++----------- .../chart_js/chart_js_state.dart | 5 ++ .../chart_js/native/chart_js_state.dart | 49 +++++++++++ .../native/unsupported_chart_js_state.dart | 12 +++ .../chart_js/web/chart_js_state.dart | 44 ++++++++++ .../ensemble/lib/widget/widget_registry.dart | 2 +- 7 files changed, 222 insertions(+), 51 deletions(-) create mode 100644 modules/ensemble/lib/util/chart_utils.dart rename modules/ensemble/lib/widget/visualization/{ => chart_js}/chart_js.dart (79%) create mode 100644 modules/ensemble/lib/widget/visualization/chart_js/chart_js_state.dart create mode 100644 modules/ensemble/lib/widget/visualization/chart_js/native/chart_js_state.dart create mode 100644 modules/ensemble/lib/widget/visualization/chart_js/native/unsupported_chart_js_state.dart create mode 100644 modules/ensemble/lib/widget/visualization/chart_js/web/chart_js_state.dart diff --git a/modules/ensemble/lib/util/chart_utils.dart b/modules/ensemble/lib/util/chart_utils.dart new file mode 100644 index 000000000..7de8b071c --- /dev/null +++ b/modules/ensemble/lib/util/chart_utils.dart @@ -0,0 +1,78 @@ +class ChartUtils { + static String getClickEventScript(String chartId, {bool isWeb = false}) { + // For web, we use the chart variable name pattern + final chartReference = isWeb ? 'myChart$chartId' : 'window.chart'; + + String messageHandler = isWeb + ? 'window.handleMessage(\'$chartId\', JSON.stringify(dataCollection));' + : 'messageHandler.postMessage(JSON.stringify(dataCollection));'; + + return ''' + try { + var activePoints = $chartReference.getElementsAtEventForMode(event, 'nearest', { intersect: true }, true); + if (activePoints.length > 0) { + var firstPoint = activePoints[0]; + var datasetIndex = firstPoint.datasetIndex; + var index = firstPoint.index; + var dataset = $chartReference.data.datasets[datasetIndex] || {}; + var dataCollection = { + data: { + label: $chartReference.data.labels[index] || '', + value: dataset.data ? dataset.data[index] : '', + datasetLabel: dataset.label || '', + datasetIndex: datasetIndex, + index: index, + backgroundColor: dataset.backgroundColor || '', + borderColor: dataset.borderColor || '', + x: firstPoint.element.x || 0, + y: firstPoint.element.y || 0, + chartType: $chartReference.config.type || '', + options: JSON.parse(JSON.stringify($chartReference.options, + function(key, value) { + return (typeof value === 'function') ? value.toString() : value; + } + )) || {} + } + }; + $messageHandler + } + } catch (error) { + console.error('Chart click handling error:', error); + ${isWeb + ? 'window.handleMessage(\'' + chartId + '\', JSON.stringify({error: error.message}));' + : 'messageHandler.postMessage(JSON.stringify({error: error.message}));' + } + } + '''; + } + + static String getBaseHtml(String chartId, String config) { + return ''' + + + + + + + + + + + + + '''; + } +} \ No newline at end of file diff --git a/modules/ensemble/lib/widget/visualization/chart_js.dart b/modules/ensemble/lib/widget/visualization/chart_js/chart_js.dart similarity index 79% rename from modules/ensemble/lib/widget/visualization/chart_js.dart rename to modules/ensemble/lib/widget/visualization/chart_js/chart_js.dart index f760c97e3..5129ed941 100644 --- a/modules/ensemble/lib/widget/visualization/chart_js.dart +++ b/modules/ensemble/lib/widget/visualization/chart_js/chart_js.dart @@ -1,14 +1,15 @@ -import 'dart:io'; -import 'dart:math'; +import 'dart:convert'; +import 'package:ensemble/framework/action.dart'; import 'package:ensemble/framework/widget/widget.dart'; +import 'package:ensemble/screen_controller.dart'; import 'package:ensemble/util/utils.dart'; import 'package:ensemble/widget/helpers/controllers.dart'; import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; import 'package:ensemble_ts_interpreter/parser/newjs_interpreter.dart'; import 'package:flutter/material.dart'; -import 'package:js_widget/js_widget.dart'; -import 'dart:convert'; +import 'chart_js_state.dart'; +import 'dart:math'; class ChartJsController extends WidgetController { ChartJsController() { @@ -21,6 +22,31 @@ class ChartJsController extends WidgetController { String get chartId => id!; dynamic config = ''; Function? evalScript; + EnsembleAction? onTap; +} + +abstract class ChartJsStateBase extends EWidgetState { + void handleChartClick(String data) { + if (widget.controller.onTap != null) { + Map eventData = json.decode(data); + widget.controller.onTap!.inputs = {'event': eventData}; + ScreenController().executeAction(context, widget.controller.onTap!); + } + } + + @override + void initState() { + super.initState(); + widget.controller.evalScript = evalScript; + } + + @override + void dispose() { + widget.controller.evalScript = null; + super.dispose(); + } + + void evalScript(String script); } class ChartJs extends StatefulWidget @@ -125,52 +151,9 @@ class ChartJs extends StatefulWidget } else { _controller.config = value; } - } - }; - } -} - -class ChartJsState extends EWidgetState { - JsWidget? jsWidget; - void evalScript(String script) { - if (jsWidget == null) { - print('evalScript is being called on a jsWidget which is null'); - } else { - jsWidget!.evalScript(script); - } - } - - @override - void initState() { - widget.controller.evalScript = evalScript; - super.initState(); - } - - @override - void dispose() { - widget.controller.evalScript = null; - super.dispose(); - } - - @override - Widget buildWidget(BuildContext context) { - if (widget.controller.config == '') { - return const Text(""); - } - jsWidget = JsWidget( - id: widget.controller.id!, - createHtmlTag: () => - '
', - scriptToInstantiate: (String c) { - return 'if (typeof ${widget.controller.chartVar} !== "undefined") ${widget.controller.chartVar}.destroy();${widget.controller.chartVar} = new Chart(document.getElementById("${widget.controller.chartId}"), $c);${widget.controller.chartVar}.update();'; }, - size: Size(widget.controller.width.toDouble(), - widget.controller.height.toDouble()), - data: widget.controller.config, - scripts: const [ - "https://cdn.jsdelivr.net/npm/chart.js", - ], - ); - return jsWidget!; + 'onTap': (funcDefinition) => _controller.onTap = + EnsembleAction.from(funcDefinition, initiator: this), + }; } } diff --git a/modules/ensemble/lib/widget/visualization/chart_js/chart_js_state.dart b/modules/ensemble/lib/widget/visualization/chart_js/chart_js_state.dart new file mode 100644 index 000000000..e8afdf2dd --- /dev/null +++ b/modules/ensemble/lib/widget/visualization/chart_js/chart_js_state.dart @@ -0,0 +1,5 @@ +library chart_js_state; + +export 'native/unsupported_chart_js_state.dart' + if (dart.library.html) 'web/chart_js_state.dart' + if (dart.library.io) 'native/chart_js_state.dart'; diff --git a/modules/ensemble/lib/widget/visualization/chart_js/native/chart_js_state.dart b/modules/ensemble/lib/widget/visualization/chart_js/native/chart_js_state.dart new file mode 100644 index 000000000..e5125c75c --- /dev/null +++ b/modules/ensemble/lib/widget/visualization/chart_js/native/chart_js_state.dart @@ -0,0 +1,49 @@ +import 'package:ensemble/util/chart_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import '../chart_js.dart'; + +class ChartJsState extends ChartJsStateBase { + late WebViewController _controller; + + @override + void evalScript(String script) { + _controller.runJavaScript(''' + try { + if (typeof window.chart !== "undefined") { + ${script.replaceAll(widget.controller.chartVar, "window.chart")} + } else { + console.error('Chart is not initialized'); + messageHandler.postMessage(JSON.stringify({error: 'Chart is not initialized'})); + } + } catch (error) { + console.error('Script execution error:', error); + messageHandler.postMessage(JSON.stringify({error: error.message})); + } + '''); + } + + @override + Widget buildWidget(BuildContext context) { + if (widget.controller.config == '') return const Text(""); + + _controller = WebViewController() + ..setBackgroundColor(Colors.transparent) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..addJavaScriptChannel( + 'messageHandler', + onMessageReceived: (JavaScriptMessage message) => + handleChartClick(message.message), + ) + ..loadHtmlString(ChartUtils.getBaseHtml( + widget.controller.chartId, + widget.controller.config, + )); + + return SizedBox( + width: widget.controller.width.toDouble(), + height: widget.controller.height.toDouble(), + child: WebViewWidget(controller: _controller), + ); + } +} diff --git a/modules/ensemble/lib/widget/visualization/chart_js/native/unsupported_chart_js_state.dart b/modules/ensemble/lib/widget/visualization/chart_js/native/unsupported_chart_js_state.dart new file mode 100644 index 000000000..f75e4de43 --- /dev/null +++ b/modules/ensemble/lib/widget/visualization/chart_js/native/unsupported_chart_js_state.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import '../chart_js.dart'; + +class ChartJsState extends ChartJsStateBase { + @override + Widget buildWidget(BuildContext context) { + return const Text("ChartJs is not supported on this platform"); + } + + @override + void evalScript(String script) {} +} diff --git a/modules/ensemble/lib/widget/visualization/chart_js/web/chart_js_state.dart b/modules/ensemble/lib/widget/visualization/chart_js/web/chart_js_state.dart new file mode 100644 index 000000000..1778c35f1 --- /dev/null +++ b/modules/ensemble/lib/widget/visualization/chart_js/web/chart_js_state.dart @@ -0,0 +1,44 @@ +import 'package:ensemble/util/chart_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:js_widget/js_widget.dart'; +import '../chart_js.dart'; + +class ChartJsState extends ChartJsStateBase { + JsWidget? jsWidget; + + void evalScript(String script) { + if (jsWidget == null) { + print('evalScript is being called on a jsWidget which is null'); + } else { + jsWidget!.evalScript(script); + } + } + + @override + Widget buildWidget(BuildContext context) { + if (widget.controller.config == '') { + return const Text(""); + } + + jsWidget = JsWidget( + id: widget.controller.id!, + createHtmlTag: () => + '
', + scriptToInstantiate: (String c) => ''' + if (typeof ${widget.controller.chartVar} !== "undefined") ${widget.controller.chartVar}.destroy(); + ${widget.controller.chartVar} = new Chart(document.getElementById("${widget.controller.chartId}"), $c); + ${widget.controller.chartVar}.update(); + + document.getElementById("${widget.controller.chartId}").onclick = function(event) { + ${ChartUtils.getClickEventScript(widget.controller.id!, isWeb: true)} + }; + ''', + size: Size(widget.controller.width.toDouble(), + widget.controller.height.toDouble()), + data: widget.controller.config, + scripts: const ["https://cdn.jsdelivr.net/npm/chart.js"], + listener: handleChartClick, + ); + return jsWidget!; + } +} diff --git a/modules/ensemble/lib/widget/widget_registry.dart b/modules/ensemble/lib/widget/widget_registry.dart index 8a68269fb..d03118311 100644 --- a/modules/ensemble/lib/widget/widget_registry.dart +++ b/modules/ensemble/lib/widget/widget_registry.dart @@ -64,7 +64,7 @@ import 'package:ensemble/widget/video.dart'; import 'package:ensemble/widget/slidable.dart'; import 'package:ensemble/widget/accordion.dart'; import 'package:ensemble/widget/visualization/barchart.dart'; -import 'package:ensemble/widget/visualization/chart_js.dart'; +import 'package:ensemble/widget/visualization/chart_js/chart_js.dart'; import 'package:ensemble/widget/visualization/line_area_chart.dart'; import 'package:ensemble/widget/visualization/topology_chart.dart'; import 'package:ensemble/widget/webview/webview.dart';