🗺️
ECharts Scatter3DをFlutterWebに実装する(地図編)
テストプロジェクトから運用中のサイトへ
前回作ったシンプルなプロジェクトで学んだことを、本番環境に実装する。
Previous Episode....
まずは「なんもかんもmain.dart」から脱却する。
main.dartのものはmain.dartに
運用中のmain.dartに特に追加するものはない。
main.dartの仕事はmain.dartに任せて、切り離す。
index.htmlに追加してある情報はこれ
index.html
index.html
<!-- Include ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<!-- Include ECharts-GL for 3D support -->
<script src="https://cdn.jsdelivr.net/npm/echarts-gl@2/dist/echarts-gl.min.js"></script>
<!-- Your custom JavaScript for initializing the chart -->
<script>
// Function to initialize the ECharts instance
function initChart(chartId, optionsJson) {
const chartDom = document.getElementById(chartId);
if (chartDom) {
const myChart = echarts.init(chartDom, 'dark');
const options = JSON.parse(optionsJson);
myChart.setOption(options);
} else {
console.error('Div element not found:', chartId);
}
}
</script>
それを読み出すコードを切り離す
echarts_js.dart
echarts_js.dart
import 'package:js/js.dart';
/// Annotate the Dart function to bind it to the JavaScript `initChart` function.
/// This allows Dart to call the JavaScript function directly.
('initChart')
external void initChart(String chartId, String optionsJson);
class MapData {
final double longitude;
final double latitude;
final double logarithm;
final int annee;
final String location;
final String precise;
final String affair;
MapData({
required this.longitude,
required this.latitude,
required this.logarithm,
required this.annee,
required this.location,
required this.precise,
required this.affair,
});
factory MapData.fromJson(Map<String, dynamic> json) {
return MapData(
longitude: json['longitude'],
latitude: json['latitude'],
logarithm: json['logarithm'],
annee: json['annee'],
location: json['location'],
precise: json['precise'],
affair: json['affair'],
);
}
Map<String, dynamic> toJson() {
return {
'longitude': longitude,
'latitude': latitude,
'logarithm': logarithm,
'annee': annee,
'location': location,
'precise': precise,
'affair': affair,
};
}
}
残ったのがmap_page
ほんとはmap_modelと分離したいのだけれど、
なぜかうまくいかないので、(爺様がやってもうまくいかない)
今回はこのまま。
map_page.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 MapPage extends StatefulWidget {
final List<int>? principalIds;
const MapPage({super.key, this.principalIds}); <= 前の頁から検索結果を受け取る。
MapPageState createState() => MapPageState();
}
class MapPageState extends State<MapPage> {
final String _viewType = 'echarts-div'; <= MAP表示ですよ、というサイン
final FetchWithMapRepository _repository = FetchWithMapRepository(); <= 検索結果のIdsを使って、位置情報を持った地図用のデータを取得する。
// Data list initialization
List<Map<String, dynamic>> _dataList = [];
void initState() {
super.initState();
// Register the view factory for the ECharts container
ui_web.platformViewRegistry.registerViewFactory(_viewType, (int viewId) {
final html.DivElement div = html.DivElement()
..id = 'echarts_div' <= MAP表示ですよ
..style.width = '100%' <= 表示領域、横幅いっぱい
..style.height = '800px'; <= 高さは指定した
// Initialize
Future.microtask(() async {
await _initializeChart(); <= 描画タイミングを調整
});
return div;
});
}
Future<void> _onFetchWithMapButtonPressed() async {
try {
// `principalIds` を使用して検索する
final keyNumbers = widget.principalIds ?? []; // null の場合は空リストを使用
if (keyNumbers.isEmpty) {
print('No principal IDs provided');
return;
}
final listWithMap = await _repository.fetchWithMap(keyNumbers: keyNumbers);
setState(() {
_dataList = listWithMap.map((d) {
return {
'value': [d.longitude, d.latitude, d.logarithm, d.annee, d.location, d.precise], <= 位置情報など
'name': d.affair <= 事象名
};
}).toList();
});
await _initializeChart();
} catch (e) {
print('Error fetching data: $e');
}
}
///Assetにある白地図情報を読み込む
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);
final Map<String, dynamic> option = _buildChartOptions(transformedCoast, transformedRidge, transformedTrench);
final String optionJson = json.encode(option);
initChart('echarts_div', optionJson);
} catch (e) {
print('Error initializing chart: $e');
}
}
///白地図情報を整形 Z軸の値は「紀元ゼロ年」
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();
}
///表示するデータ 白地図のデータ明細は出さない 検索結果は出す
Map<String, dynamic> _buildChartOptions(List<List<double>> coast, List<List<double>> ridge, List<List<double>> trench) {
return {
'tooltip': { <= マウスが乗ったときに表示されるデータ明細のデザイン 全体設定
'trigger': 'item' <= 個別に設定するよ予告
},
'xAxis3D': {
'type': 'value',
'name': 'Longitude',
'min': -180,
'max': 180,
'splitNumber': 6
},
'yAxis3D': {
'type': 'value',
'name': 'Latitude',
'min': -90,
'max': 90,
'splitNumber': 2
},
'zAxis3D': {
'type': 'value',
'name': 'Timeline',
'min': -5000,
'max': 2000,
'splitNumber': 2
},
'grid3D': {
'axisLine': {
'lineStyle': {'color': '#fff'},
},
'axisPointer': {
'lineStyle': {'color': '#ffbd67'},
},
'boxWidth': 360,
'boxDepth': 180,
'boxHeight': 180,
'viewControl': {
'projection': 'orthographic',
'targetCoord': [0, 0, 0],
'alpha': 180,
'beta': 0,
'zoom': 1.5,
'autoRotate': false,
},
},
'series': [
{
'name': 'Coast',
'type': 'scatter3D',
'data': coast,
'symbolSize': 3,
'itemStyle': {'color': 'white'}
'tooltip': {'show': false} <= Pop Upの個別設定 表示しない
},
{
'type': 'scatter3D',
'data': ridge,
'symbolSize': 3,
'itemStyle': {'color': '#bc8f8f'}
'tooltip': {'show': false} <= Pop Upの個別設定 ここも表示しない
},
{
'type': 'scatter3D',
'data': trench,
'symbolSize': 3,
'itemStyle': {'color': '#cd5c5c'}
'tooltip': {'show': false} <= Pop Upの個別設定 ここも表示しない
},
if (_dataList.isNotEmpty)
{
'type': 'scatter3D',
'data': _dataList,
'symbolSize': 8,
'dimensions': ['Longitude', 'Latitude', 'Logarithm', 'Year', 'Location', 'Precise'],
'itemStyle': {'color': 'yellow'},
'tooltip': {'show': true}, <= Pop Upの個別設定 ここは表示する
}
],
};
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Results on Map (Atlantic centered) '),
),
body: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/both.png'),
fit: BoxFit.cover,
)),
child: Column(
children: [
Expanded(child: HtmlElementView(viewType: _viewType)), <= ここに地図が表示される
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: ElevatedButton(
onPressed: _onFetchWithMapButtonPressed,
child: const Text('Show on Map'),
),
),
const Padding(
padding: EdgeInsets.only(bottom: 50),
child: Text(
'You can zoom in, zoom out, and rotate the view. \nHowever, we currently do not support adjustments when the view is cut off due to zooming in.\n In such cases, please either zoom out or use the Pacific-centered map or the globe view.',
style: TextStyle(color: Colors.white),
),
),
],
),
),
);
}
}
うまくいったこと
白地図のデータは出さずに、検索結果の詳細は表示できるようになった。
まだできないこと
検索結果の詳細をカスタマイズしたいんだが、うまくいかない。
(爺様がやってもうまくいかない)
困っていること
地図を拡大すると、上下左右が見切れる。
これをスクロールでカバーしようとすると、回転と競合して動かなくなる。
まあ、当たり前。同じ動きに対して、二つの動きがあるわけだから。
スクロールかローテーションか選ぶボタンでもつけたら、どうにかなるかしら。
いったん保留。
さあ、次は地球儀を実装するよ〜
四次元年表
Discussion