#1.はじめに
csvデータをアップロードし、グラフ化・地図表示して表示する、というシンプルなコードが、意外となかったため、本記事を作成。
完成イメージを以下に示す。
Fig.1の上半分が、amchart.jsの折れ線グラフを使って作成した。x軸、y軸ともに、表示範囲を自由に選択できる。x軸は、時間軸である。
Fig.2の下半分は、GoogleMapAPIを活用して、緯度・経度データを入力して表示できるようにした。
#2.使ったサービス・フレームワークなど
・Google Map API
・Django
・amchart.js
#3.実装
#3-1.構築環境
環境・バージョンは以下の通り。
macOS Catalina version 10.15.4
Python 3.7.0
#3-2.ディレクトリ構成
─root─app─templates─app─base.html
│ | └import.html
│ ├migrations-・・・
│ ├forms.py
│ ├views.py
│ ・・・・
|
|
├project─settings.py
| ├urls.py
| ・・・
|
├static─js─mychart.js
| | └googlemap.js
| └media-temp.csv (<-インポートしたcsvファイルを一時補完するファイル)
└manage.py
Djangoの基本ディレクトリ構成に従っている。
#3-3. コード
import csv
from django.http import HttpResponse
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views import generic
from .forms import CSVUploadForm
from .models import Post
class PostIndex(generic.ListView):
model = Post
class PostImport(generic.FormView):
template_name = 'app/import.html'
success_url = reverse_lazy('app:index')
form_class = CSVUploadForm
def form_valid(self, form):
form.save()
return redirect('app:index')
def post_export(request):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="posts.csv"'
# HttpResponseオブジェクトはファイルっぽいオブジェクトなので、csv.writerにそのまま渡せます。
writer = csv.writer(response)
for post in Post.objects.all():
writer.writerow([post.pk, post.title])
return response
forms.pyが、一番工夫したソースコードになる。
import csv
import io
from django import forms
from django.core.validators import FileExtensionValidator
from .models import Post
import codecs
class CSVUploadForm(forms.Form):
file = forms.FileField(
label='CSVファイル',
help_text='※拡張子csvのファイルをアップロードしてください。',
validators=[FileExtensionValidator(allowed_extensions=['csv'])]
)
def clean_file(self):
file = self.cleaned_data['file']
print("file:",file)
# csv.readerに渡すため、TextIOWrapperでテキストモードなファイルに変換
csv_file = io.TextIOWrapper(file, encoding='utf-8')
print("csv_file",csv_file)
reader = csv.reader(csv_file)
print("reader",reader)
self.csv_file = reader
for row in reader:
#print("reader row", row)
#print("line_num", reader.line_num)
if reader.line_num == 1:
with open("static/media/temp.csv", mode="w", encoding="utf-8") as f:
print("row in x before", row)
row = ",".join(row)
row = row + "\n"
print("row in x after", row)
f.write(row)
print("option=x")
else:
with open("static/media/temp.csv", mode="a", encoding="utf-8") as f:
#print("row in a before", row)
#print(type(row))
row = ",".join(row)
row = row + "\n"
#print("row in a after", row)
#print(type(row))
f.write(row)
#print("option=a")
# 各行から作った保存前のモデルインスタンスを保管するリスト
self._instances = []
try:
for row in reader:
post = Post(pk=row[0], title=row[1])
self._instances.append(post)
except UnicodeDecodeError:
raise forms.ValidationError('ファイルのエンコーディングや、正しいCSVファイルか確認ください。')
return file
def save(self):
Post.objects.bulk_create(self._instances, ignore_conflicts=True)
Post.objects.bulk_update(self._instances, fields=['title'])
print("save content in save func", Post)
from django.urls import path
from . import views
app_name = 'app'
urlpatterns = [
path('', views.PostIndex.as_view(), name='index'),
path('import/', views.PostImport.as_view(), name='import'),
path('export/', views.post_export, name='export'),
]
以下のソースのうち、[API-KEY]はAPI-Keyを挿入する。Google_API-Keyは、各自で発行ください。
<!doctype html>
{% load static %}
<html lang="ja">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<style>
#chartdiv {
width: 100%;
height: 500px;
}
#map {
width: 100%;
height: 400px;
}
</style>
<!-- Resources -->
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>
<!-- Chart code -->
<script src="{% static 'js/mychart.js' %}"></script>
<title>ローバーセンサ 可視化</title>
</head>
<body>
<ul class="nav justify-content-center">
<li class="nav-item">
<a class="nav-link" href="{% url 'app:index' %}">Index</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'app:import' %}">CSV読み込み</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'app:export' %}">CSV出力</a>
</li>
</ul>
<div class="container">
{% block content %}{% endblock %}
<div id="map"></div>
<script src="{% static 'js/googlemap.js' %}"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=[API-KEY]"
async defer></script>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
</div>
</body>
</html>
{% extends 'app/base.html' %}
{% block content %}
<form action="" method="POST" enctype="multipart/form-data">
{{ form.as_ul }}
{% csrf_token %}
<button type="submit">送信</button>
</form>
{% endblock %}
{% extends 'app/base.html' %}
{% block content %}
<div id="chartdiv"></div>
{% endblock %}
// 2) CSVから2次元配列に変換
function csv2Array(str) {
var csvData = [];
var lines = str.split("\n");
for (var i = 0; i < lines.length; ++i) {
var cells = lines[i].split(",");
csvData.push(cells);
}
return csvData;
}
function drawBarChart(data) {
// 3)chart.jsのdataset用の配列を用意
var tmpLabels = [], tmpData1 = [], tmpData2 = [];
for (var row in data) {
tmpLabels.push(data[row][0])
tmpData1.push(data[row][1])
tmpData2.push(data[row][2])
};
// 4)chart.jsで描画
var ctx = document.getElementById("myChart").getContext("2d");
var myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: tmpLabels,
datasets: [
{ label: "Tokyo", data: tmpData1, backgroundColor: "red" },
{ label: "Osaka", data: tmpData2, backgroundColor: "blue" }
]
}
});
}
function makeLineChart(data) {
am4core.ready(function() {
// Themes begin
am4core.useTheme(am4themes_animated);
// Themes end
// Create chart instance
var chart = am4core.create("chartdiv", am4charts.XYChart);
// Add data
console.log(data);
console.log(typeof data);
console.log(data[1][1]);
chart.data = generateChartData(data); // 追加
console.log(chart.data);
// Create axes
var xAxis = chart.xAxes.push(new am4charts.ValueAxis());
//xAxis.renderer.minGridDistance = 150;
xAxis.title.text = "Time (sec)";
var yAxis = chart.yAxes.push(new am4charts.ValueAxis());
yAxis.title.text = "ax";
// Create series
var series = chart.series.push(new am4charts.LineSeries());
series.dataFields.valueX = "timestamp";
series.dataFields.valueY ="ax";
series.name = "ax";
series.strokeWidth = 2;
series.minBulletDistance = 10;
series.tooltipText = "{valueY}";
series.tooltip.pointerOrientation = "vertical";
series.tooltip.background.cornerRadius = 20;
series.tooltip.background.fillOpacity = 0.5;
series.tooltip.label.padding(12,12,12,12)
var series2 = chart.series.push(new am4charts.LineSeries());
series2.dataFields.valueX = "timestamp";
series2.dataFields.valueY = "ay";
series2.name = "ay";
var series3 = chart.series.push(new am4charts.LineSeries());
series3.dataFields.valueX = "timestamp";
series3.dataFields.valueY = "az";
series3.name = "az";
// Create y axis range
var range = yAxis.axisRanges.create();
range.label.disabled = false;
range.label.rotation = 270;
// Create x axis range
var range_x = xAxis.axisRanges.create();
range_x.label.disabled = false;
range_x.label.rotation = 0;
// Add scrollbar
chart.scrollbarX = new am4charts.XYChartScrollbar();
chart.scrollbarX.series.push(series);
chart.scrollbarX.series.push(series2);
chart.scrollbarX.series.push(series3);
chart.scrollbarY = new am4charts.XYChartScrollbar();
chart.scrollbarY.series.push(series);
chart.scrollbarY.series.push(series2);
chart.scrollbarY.series.push(series3);
// Add cursor
chart.cursor = new am4charts.XYCursor();
chart.cursor.xAxis = xAxis;
chart.cursor.yAxis = yAxis;
chart.cursor.snapToSeries = series;
chart.cursor.snapToSeries = series2;
chart.cursor.snapToSeries = series3;
// Add legend
chart.legend = new am4charts.Legend();
function generateChartData(data) {
var chartData = [];
var index_length = Number(data.length);
const initial_time = data[0][0]
console.log(index_length);
for (var i = 0; i < (index_length-1); i++) {
var data_line = data[i];
chartData.push({
timestamp: (data_line[0]-initial_time)/1000,
ax: data_line[1],
ay: data_line[2],
az: data_line[3],
wx: data_line[4],
wy: data_line[5],
wz: data_line[6],
mx: data_line[7],
my: data_line[8],
mz: data_line[9],
lat: data_line[10],
lng: data_line[11],
yaw: data_line[12]
});
}
return chartData;
}
});
}
function main(file_name) {
// 1) ajaxでCSVファイルをロード
var req = new XMLHttpRequest();
var filePath = file_name//'acc_gyro.csv';
req.open("GET", filePath, true);
req.onload = function() {
// 2) CSVデータ変換の呼び出し
data = csv2Array(req.responseText);
// 3) amChart.jsデータ準備、4) amChart.js描画の呼び出し
makeLineChart(data);
}
req.send(null);
}
main("/static/media/temp.csv");
// 2) CSVから2次元配列に変換
function csv2Array(str) {
var csvData = [];
var lines = str.split("\n");
for (var i = 0; i < lines.length; ++i) {
var cells = lines[i].split(",");
csvData.push(cells);
}
return csvData;
}
// 2次元配列にプロパティと
function mapData(data) {
var mapData = [];
const initial_time = data[0][0]
var index_length = Number(data.length);
for (var i = 0; i < (index_length-1); i++) {
var data_line = data[i];
mapData.push({
timestamp: (data_line[0]-initial_time)/1000,
ax: data_line[1],
ay: data_line[2],
az: data_line[3],
wx: data_line[4],
wy: data_line[5],
wz: data_line[6],
mx: data_line[7],
my: data_line[8],
mz: data_line[9],
lat: data_line[10],
lng: data_line[11],
yaw: data_line[12]
});
}
return mapData;
}
function initMap(data) {
var map;
mapdata = mapData(data);
var index_length = Number(data.length);
console.log(mapdata);
console.log(index_length);
initial_loc = mapdata[0];
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: Number(initial_loc.lat), lng: Number(initial_loc.lng)},
zoom: 15
});
for(var i = 1; i < (index_length-1); i++) {
location_line = mapdata[i];
//console.log(i);
//console.log(location_line);
// Markerの初期設定
var markerOpts = {
position: {lat: Number(location_line.lat), lng: Number(location_line.lng)},
map: map,
title: "mark"
};
// 直前で作成したMarkerOptionsを利用してMarkerを作成
var marker = new google.maps.Marker(markerOpts);
}
}
function main(file_name) {
// 1) ajaxでCSVファイルをロード
var req = new XMLHttpRequest();
var filePath = file_name;//'static/js/acc_gyro.csv';
req.open("GET", filePath, true);
req.onload = function() {
// 2) CSVデータ変換の呼び出し
data = csv2Array(req.responseText);
// 3) google mapデータ準備、4) google map描画の呼び出し
initMap(data);
}
req.send(null);
}
main("/static/media/temp.csv");
(前略)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "static"),
)
#4.所感
・Djangoを使って、初めて作ったwebアプリになる。感慨深い。
・staticディレクトリの設定方法は、tutorialに記載がなかったので、非常に勉強になった。
・次は、AWSのEC2インスタンスへのデプロイ結果を報告したい。
#5.参考文献
amchartの実装方法:https://www.suzu6.net/posts/56-amcharts-samples/
csvアップロード機能の実装:https://qiita.com/t-iguchi/items/d2862e7ef7ec7f1b07e5