ECharts Scatter3DをFlutterWebに実装する(地球儀編)
iOSアプリで偶然できたなんちゃって実装とはぜんぜん違っていた
基本は地図と一緒でしょ?
map_pageから「ちょっと」書き換えてglobe_pageをつくればいいわけでしょ?
index.htmlやそれを読むecharts_js.dartは変更なし。
ということで、ともかく書いてみたコードがこちら。
import 'dart:convert';
import 'dart:html' as html;
import 'dart:ui_web' as ui_web;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../fetch/fetch_with_map.dart';
import 'echarts_js.dart';
class GlobePage extends StatefulWidget {
final List<int>? principalIds;
const GlobePage({super.key, this.principalIds});
GlobePageState createState() => GlobePageState();
}
class GlobePageState extends State<GlobePage> {
final FetchWithMapRepository _repository = FetchWithMapRepository();
// ECharts を表示するための viewType 名
final String _viewType = 'echarts-div-globe'; <= 地図じゃないよ、地球儀だよ
// 実際にプロットするデータを保持
// (repository.fetchWithMap で取得したデータを _onFetchWithMapButtonPressed でセットする想定)
List<Map<String, dynamic>> _dataList = [];
void initState() {
super.initState();
// Globe 用の ECharts コンテナを登録
ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId) {
final html.DivElement div = html.DivElement()
..id = 'echarts_div_globe' <= 地球儀だよ
..style.width = '100%' <= 表示領域は地図と同じ
..style.height = '800px';
// 画面描画直後に ECharts 初期化を実行
Future.microtask(() async {
await _initializeChart();
});
return div;
});
}
/// ボタン押下時にバックエンドからデータを取得し、_dataList をセット
Future<void> _onFetchWithMapButtonPressed() async {
try {
// widget.principalIds から ID リストを取得
final keyNumbers = widget.principalIds ?? [];
if (keyNumbers.isEmpty) {
debugPrint('No principal IDs provided');
return;
}
// データ取得
final listWithMap = await _repository.fetchWithMap(keyNumbers: keyNumbers);
setState(() {
// ECharts が期待する [longitude, latitude, 他] 形式へ格納
_dataList = listWithMap
.map((d) => {
'name': d.affair, // ツールチップなどで表示したい名称
'value': [
d.longitude,
d.latitude,
d.logarithm, // 時系列を示す数値
d.annee,
d.location,
d.precise
],
})
.toList();
});
// データを更新したので再度チャートを初期化(再描画)
await _initializeChart();
} catch (e) {
debugPrint('Error fetching data: $e');
}
}
/// ECharts のオプション生成と initChart 呼び出し、assetsから白地図を呼び出す
Future<void> _initializeChart() async {
try {
final String coastLineString = await rootBundle.loadString('assets/json/coastline.json');
final List<dynamic> coastLineData = json.decode(coastLineString);
final String ridgeLineString = await rootBundle.loadString('assets/json/ridge.json');
final List<dynamic> ridgeLineData = json.decode(ridgeLineString);
final String trenchLineString = await rootBundle.loadString('assets/json/trench.json');
final List<dynamic> trenchLineData = json.decode(trenchLineString);
final transformedCoast = _transformData(coastLineData);
final transformedRidge = _transformData(ridgeLineData);
final transformedTrench = _transformData(trenchLineData);
// ECharts 用のオプションを作成
final Map<String, dynamic> option = _buildChartOptions(transformedCoast, transformedRidge, transformedTrench);
// Dart のマップを JSON に変換して、JS 側に渡す
final String optionJson = json.encode(option);
initChart('echarts_div_globe', optionJson);
} catch (e) {
debugPrint('Error initializing chart: $e');
}
}
///白地図データを整形
List<List<double>> _transformData(List<dynamic> data) {
return data.map((item) {
final double lon = (item[0] is num) ? item[0].toDouble() : double.tryParse(item[0].toString()) ?? 0.0;
final double lat = (item[1] is num) ? item[1].toDouble() : double.tryParse(item[1].toString()) ?? 0.0;
return [lon, lat, 0.0];
}).toList();
}
/// Globe で表示する際の ECharts オプションを定義
Map<String, dynamic> _buildChartOptions(List<List<double>> coast, List<List<double>> ridge, List<List<double>> trench) {
return {
'tooltip': {
'trigger': 'item' <= 地図と同じ、グローバルのTooltipは使わない
},
'globe': {
'environment': '#000000', // 背景
'shading': 'lambert',
'light': {
'main': {
'intensity': 0.2, // 明るいと白地図が飛ぶので暗め
},
},
'viewControl': {
'autoRotate': false, <= 勝手に自転しない
'alpha': 30,
'beta': 160,
'zoomSensitivity': 2,
},
},
'series': [
{
'type': 'scatter3D',
'coordinateSystem': "globe",
'blendMode': "lighter",
'symbolSize': 5,
'itemStyle': {
'color': "white",
'opacity': 1
},
'data': coast,
'tooltip': {'show': false} <= 白地図情報には表示しない
},
{
'type': 'scatter3D',
'coordinateSystem': "globe",
'blendMode': "lighter",
'symbolSize': 5,
'itemStyle': {
'color': "#adff2f",
'opacity': 1
},
'data': ridge,
'tooltip': {'show': false}
},
{
'type': 'scatter3D',
'coordinateSystem': "globe",
'blendMode': "lighter",
'symbolSize': 5,
'itemStyle': {
'color': "#008000",
'opacity': 1
},
'data': trench,
'tooltip': {'show': false}
},
{
'type': 'scatter3D',
'coordinateSystem': 'globe',
// 取得したデータをそのまま描画
'data': _dataList,
'symbolSize': 8,
// value の各要素名を指定しておくとデバッグ・ツールで見やすい
'dimensions': [
'Longitude',
'Latitude',
'Logarithm',
'Year',
'Location',
'Precise'
],
// 色やマテリアルに関する設定
'itemStyle': {
'color': 'yellow',
'opacity': 0.8,
},
'tooltip': {'show': true} <= 検索結果は詳細表示
}
],
};
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Results on Globe'),
),
body: Container(
color: Colors.black,
child: Column(
children: [
// Globe を表示する領域
Expanded(
child: HtmlElementView(viewType: _viewType),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _onFetchWithMapButtonPressed,
child: const Text('Show on Globe'),
),
const Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'You can rotate or zoom in/out the globe.\nTap "Show on Globe" to fetch and display the data by principal IDs.',
style: TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
],
),
),
);
}
}
予期せぬできごと
書いてみたら、ちゃんと表示できたのです。
ところがアプリの時と見た感じがずいぶん違う。
しかも、検索結果によっても、見え方がぜんぜん違う。
何が起こっているんだろう。
いろんな検索をして、いろんな表示をしてみた結果、
おおおお、これはすごいことが起こっているかもしれない。
当分無理だと思っていたことが、勝手にできたかもしれない!?
defaultの球体に海岸線が張りついている。
検索結果を載せると、突然こうなる。
球体から浮き上がる地球儀
そしてデータが点々とプロットされているのだが、
なんと、時代の古いものは地球儀表面より内側、
新しいものは宇宙に向かって外側に広がっている。
しかも、検索結果によって、球体から地球儀やデータの離れ方が変わる。
よく観察してみると、一番昔のデータが球体表面にあって、
一番新しいデータが表示限界ギリギリまで広がっているらしい。
それをなんと、自動で判別して、表示しているのだ!
アプリの時は、全てのデータが球体に貼り付いていたから
時間的な表現はできないなあ、と思っていたのに・・・・!
これが、見やすい、わかりやすいかといえば疑問だけれど
私は、自分がこれをやりたくてやっていて、ここまで来たので
大感激なのだけれど、
「何やってるのかわからない」という人には、やっぱりかなりわかりにくいだろう、と思う。
とくに、地球儀が球から浮き上がってしまったことで、
地球儀の背面が透けて見えるから、まるでごちゃごちゃになっている。
それに、位置情報が放射状に広がっているから、
球に対して、大陸とかがすごく大きく見えるしね・・・。
これを今後、どう見やすく、わかりやすくしていけばいいのか、まだまだ謎。
でもともかく、ここまで来た。
一度このままdeployしてみよう。
さーて、今度は地球儀を外から見るのじゃなく、中に入るよ。
いよいよVRの出番なのであります。
四次元年表
Discussion