diff --git a/modules/ensemble/lib/widget/visualization/chart_js.dart b/modules/ensemble/lib/widget/visualization/chart_js.dart index f760c97e..b2f9f3a7 100644 --- a/modules/ensemble/lib/widget/visualization/chart_js.dart +++ b/modules/ensemble/lib/widget/visualization/chart_js.dart @@ -1,14 +1,15 @@ -import 'dart:io'; +import 'dart:convert'; import 'dart:math'; - +import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/event.dart'; +import 'package:ensemble/screen_controller.dart'; +import 'package:ensemble_ts_interpreter/parser/newjs_interpreter.dart'; +import 'package:flutter/material.dart'; +import 'package:js_widget/js_widget.dart'; import 'package:ensemble/framework/widget/widget.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'; class ChartJsController extends WidgetController { ChartJsController() { @@ -21,6 +22,7 @@ class ChartJsController extends WidgetController { String get chartId => id!; dynamic config = ''; Function? evalScript; + EnsembleAction? onTap; } class ChartJs extends StatefulWidget @@ -125,7 +127,9 @@ class ChartJs extends StatefulWidget } else { _controller.config = value; } - } + }, + 'onTap': (funcDefinition) => _controller.onTap = + EnsembleAction.from(funcDefinition, initiator: this), }; } } @@ -161,15 +165,72 @@ class ChartJsState extends EWidgetState { 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();'; + scriptToInstantiate: (String config) { + return ''' + if (typeof ${widget.controller.chartVar} !== "undefined") { + ${widget.controller.chartVar}.destroy(); + } + ${widget.controller.chartVar} = new Chart(document.getElementById("${widget.controller.chartId}"), $config); + + // Add click event listener to the chart + document.getElementById("${widget.controller.chartId}").onclick = function(event) { + var activePoints = ${widget.controller.chartVar}.getElementsAtEventForMode(event, 'nearest', { intersect: true }, true); + if (activePoints.length > 0) { + var firstPoint = activePoints[0]; + var datasetIndex = firstPoint.datasetIndex; + var index = firstPoint.index; + var dataset = ${widget.controller.chartVar}.data.datasets[datasetIndex] || {}; + var label = ${widget.controller.chartVar}.data.labels[index] || ''; + var value = dataset.data ? dataset.data[index] : ''; + var datasetLabel = dataset.label || ''; + var backgroundColor = dataset.backgroundColor || ''; + var borderColor = dataset.borderColor || ''; + var x = firstPoint.element.x || 0; + var y = firstPoint.element.y || 0; + var chartType = ${widget.controller.chartVar}.config.type || ''; + // Serialize options safely + var options = JSON.parse(JSON.stringify(${widget.controller.chartVar}.options, function(key, value) { + if (typeof value === 'function') { + return value.toString(); + } + return value; + })) || {}; + var data = { + label: label, + value: value, + datasetLabel: datasetLabel, + datasetIndex: datasetIndex, + index: index, + backgroundColor: backgroundColor, + borderColor: borderColor, + x: x, + y: y, + chartType: chartType, + options: options + }; + if (window.sendMessageToFlutter) { + window.sendMessageToFlutter(JSON.stringify(data)); + } else { + console.log("Flutter handler not available"); + } + } + }; + + ${widget.controller.chartVar}.update(); + '''; }, - size: Size(widget.controller.width.toDouble(), - widget.controller.height.toDouble()), + size: Size(widget.controller.width.toDouble(), widget.controller.height.toDouble()), data: widget.controller.config, scripts: const [ "https://cdn.jsdelivr.net/npm/chart.js", ], + listener: (msg) { + if (widget.controller.onTap != null) { + Map data = jsonDecode(msg); + ScreenController().executeAction(context, widget.controller.onTap!, + event: EnsembleEvent(widget, data: data)); + } + }, ); return jsWidget!; } diff --git a/modules/js_widget/lib/src/mobile/js_widget.dart b/modules/js_widget/lib/src/mobile/js_widget.dart index a80bf3f5..250e8258 100644 --- a/modules/js_widget/lib/src/mobile/js_widget.dart +++ b/modules/js_widget/lib/src/mobile/js_widget.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; // Import for Android features. @@ -75,6 +77,14 @@ class JsWidgetState extends State { controller = WebViewController.fromPlatformCreationParams(params) ..setBackgroundColor(Colors.transparent) ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..addJavaScriptChannel( + 'JsBridge', + onMessageReceived: (JavaScriptMessage message) { + if (widget.listener != null) { + widget.listener!(message.message); + } + }, + ) ..setNavigationDelegate( NavigationDelegate( onProgress: (int progress) { @@ -151,6 +161,14 @@ class JsWidgetState extends State { for (String src in widget.scripts) { html += ''; } + html += ''' + + '''; html += ''; return html; } @@ -160,7 +178,8 @@ class JsWidgetState extends State { _isLoaded = true; }); controller.runJavaScript(''' + ${widget.preCreateScript != null ? widget.preCreateScript!() : ''} ${widget.scriptToInstantiate(widget.data)} - '''); + '''); } } diff --git a/modules/js_widget/lib/src/web/js_widget.dart b/modules/js_widget/lib/src/web/js_widget.dart index 74f361e3..43f21dd6 100644 --- a/modules/js_widget/lib/src/web/js_widget.dart +++ b/modules/js_widget/lib/src/web/js_widget.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:html' as html; +import 'dart:js' as js; // Import dart:js for interop import 'dart:math'; import 'dart:ui' as ui; @@ -27,12 +28,12 @@ class JsWidget extends StatefulWidget { ///Custom `loader` widget, until script is loaded /// - ///Has no effect on Web + /// Has no effect on Web /// - ///Defaults to `CircularProgressIndicator` + /// Defaults to `CircularProgressIndicator` final Widget loader; - ///Widget data + /// Widget data final String id; final Function scriptToInstantiate; final Function createHtmlTag; @@ -40,16 +41,16 @@ class JsWidget extends StatefulWidget { final String data; Function(String msg)? listener; - ///Widget size + /// Widget size /// - ///Height and width of the widget is required + /// Height and width of the widget is required /// - ///```dart - ///Size size = Size(400, 300); - ///``` + /// ```dart + /// Size size = Size(400, 300); + /// ``` final Size size; - ///Scripts to be loaded + /// Scripts to be loaded final List scripts; @override JsWidgetState createState() => JsWidgetState(); @@ -85,6 +86,13 @@ class JsWidgetState extends State { } } + void init(Function(String id, String msg) globalListener) { + // Expose the 'sendMessageToFlutter' function to JavaScript + js.context['sendMessageToFlutter'] = (dynamic msg) { + globalListener(widget.id, msg as String); + }; + } + @override void didUpdateWidget(covariant JsWidget oldWidget) { if (oldWidget.data != widget.data || @@ -98,9 +106,9 @@ class JsWidgetState extends State { @override void initState() { + init(globalListener); if (widget.listener != null) { addListener(widget.id, widget.listener!); - init(globalListener); } if (widget.preCreateScript != null) { eval(widget.preCreateScript!()); @@ -124,9 +132,10 @@ class JsWidgetState extends State { @override Widget build(BuildContext context) { return SizedBox( - height: widget.size.height, - width: widget.size.width, - child: HtmlElementView(viewType: widget.id)); + height: widget.size.height, + width: widget.size.width, + child: HtmlElementView(viewType: widget.id), + ); } Future _load() {