Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor chartJS implementation for web and native to implement onTap #1711

Merged
merged 3 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading