Skip to content

Commit

Permalink
Merge pull request #1711 from EnsembleUI/chartJSAgain0x45
Browse files Browse the repository at this point in the history
 refactor chartJS implementation for web and native to implement `onTap`
  • Loading branch information
TheNoumanDev authored Nov 13, 2024
2 parents 6ec51e6 + 506f6f3 commit 73791e9
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 51 deletions.
78 changes: 78 additions & 0 deletions modules/ensemble/lib/util/chart_utils.dart
Original file line number Diff line number Diff line change
@@ -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 '''
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; }
canvas { width: 100% !important; height: 100% !important; }
</style>
</head>
<body>
<canvas id="$chartId"></canvas>
<script>
try {
window.chart = new Chart(document.getElementById("$chartId"), $config);
document.getElementById("$chartId").onclick = function(event) {
${getClickEventScript(chartId)}
};
} catch (error) {
console.error('Chart initialization error:', error);
messageHandler.postMessage(JSON.stringify({error: error.message}));
}
</script>
</body>
</html>
''';
}
}
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -21,6 +22,31 @@ class ChartJsController extends WidgetController {
String get chartId => id!;
dynamic config = '';
Function? evalScript;
EnsembleAction? onTap;
}

abstract class ChartJsStateBase extends EWidgetState<ChartJs> {
void handleChartClick(String data) {
if (widget.controller.onTap != null) {
Map<String, dynamic> 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
Expand Down Expand Up @@ -125,52 +151,9 @@ class ChartJs extends StatefulWidget
} else {
_controller.config = value;
}
}
};
}
}

class ChartJsState extends EWidgetState<ChartJs> {
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: () =>
'<div id="${widget.controller.chartDiv}"><canvas id="${widget.controller.chartId}"></canvas></div>',
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),
};
}
}
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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),
);
}
}
Original file line number Diff line number Diff line change
@@ -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) {}
}
Original file line number Diff line number Diff line change
@@ -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: () =>
'<div id="${widget.controller.chartDiv}"><canvas id="${widget.controller.chartId}"></canvas></div>',
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!;
}
}
2 changes: 1 addition & 1 deletion modules/ensemble/lib/widget/widget_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 73791e9

Please sign in to comment.