UploadしてPlotするだけの簡単なhttp経由の可視化ツールを作った

# Just Upload & Plot. Easy visualize tool via http.
UplodしてPlotするだけの簡単なhttp経由の可視化ツールです。

csvまたはxlsファイルをブラウザ上のUpload欄に上げるだけでさまざまな形式のグラフを描きます。

![Peek 2019-05-22 23-05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113494/ef78acfe-eab5-5991-6de0-9197a6d73291.gif)


## csv, xlsを作成時の注意
* 1行目の1列目はx軸のタイトルになります。
* 1列目の2行目以降はx軸になります。
* 1行目の2列目以降は凡例になります。
* 2行目以降の2列目以降がデータになります。
* ファイル名はグラフタイトルになります。
* ファイル名に`_`が含まれている時、最初の`_`で区切られて、前半部分がグラフタイトル、後半部分がy軸のタイトルになります。


## 対応しているグラフ形式
* 'Line'
* 'Bar'
* 'Histogram'
* 'Pie'
* 'Polar'
* 'Box'
* 'Heatmap'
* '3D Scatter'
* '3D Surface'
* '2D Histogram'


## 対応予定のグラフ形式
* 'Contour'
* 'Candlestick'


# Install

## pip
あとでpypi登録予定

## Github
[GitHub u1and0/uplot](https://github.com/u1and0/uplot)
クローン後、`$ python uplot.py`


## Dockerhub
[Dockerhub u1and0/uplot](https://hub.docker.com/r/u1and0/uplot)


```sh
$ sudo docker pull u1and0/uplot
$ sudo docker run -d -p 8880:8880 u1and0/uplot
```

# USAGE
1. サーバーを立ち上げたらブラウザに`http//:localhost:8880`と打ち込みます。
2. csvかxlsで作成したファイルをドラッグ・アンド・ドロップしてグラフ種類を選択します。


# ScreenShots
![pic1](https://raw.githubusercontent.com/u1and0/uplot/pic/Screenshot%20from%202019-05-22%2022-54-04.png)
![pic2](https://raw.githubusercontent.com/u1and0/uplot/pic/Screenshot%20from%202019-05-22%2022-54-40.png)
![pic3](https://raw.githubusercontent.com/u1and0/uplot/pic/Screenshot%20from%202019-05-22%2022-55-05.png)
![pic4](https://raw.githubusercontent.com/u1and0/uplot/pic/Screenshot%20from%202019-05-22%2022-55-56.png)
![pic5](https://raw.githubusercontent.com/u1and0/uplot/pic/Screenshot%20from%202019-05-22%2022-56-11.png)
![pic6](https://raw.githubusercontent.com/u1and0/uplot/pic/Screenshot%20from%202019-05-22%2022-56-24.png)
![pic7](https://raw.githubusercontent.com/u1and0/uplot/pic/Screenshot%20from%202019-05-22%2022-56-41.png)






# 作り方
python製
dash (= plotly + flask)でHTMLパーツを配置していって、plotlyで可視化。サーバーをflaskで立てる。

```python:uplot.py
#!/usr/bin/env python3
import base64
import datetime
import io
import os
from collections import defaultdict

import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
import dash_table

import pandas as pd
import plotly.graph_objs as go

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

CHART_LIST = [
    'Line',
    'Bar',
    'Histogram',
    'Pie',
    'Polar',
    'Box',
    'Heatmap',
    # 'Contour',
    # 'Candlestick',
    '3D Scatter',
    '3D Surface',
    '2D Histogram',
]
CHART_LIST.sort()

app.layout = html.Div(
    [
        # File upload bunner
        dcc.Upload(
            id='upload-data',
            children=html.Div(['Drag and Drop or ',
                               html.A('Select Files')]),
            style={
                'width': '100%',
                'height': '60px',
                'lineHeight': '60px',
                'borderWidth': '1px',
                'borderStyle': 'dashed',
                'borderRadius': '5px',
                'textAlign': 'center',
                'margin': '10px'
            },
            # Allow multiple files to be uploaded
            multiple=True),
        html.H6('chart-type'),
        dcc.Dropdown(id='chart-type',
                     options=[{
                         'label': i,
                         'value': i
                     } for i in CHART_LIST],
                     value='Line'),
        html.H6('x-axis'),
        dcc.RadioItems(id='xaxis-type',
                       options=[{
                           'label': i,
                           'value': i
                       } for i in ['linear', 'log', 'category']],
                       value='linear',
                       labelStyle={'display': 'inline-block'}),
        html.H6('y-axis'),
        dcc.RadioItems(id='yaxis-type',
                       options=[{
                           'label': i,
                           'value': i
                       } for i in ['linear', 'log', 'category']],
                       value='linear',
                       labelStyle={'display': 'inline-block'}),
        html.Div(id='the_graph'),
        html.Div(id='output-data-upload'),
    ], )


def data_graph(
        df,
        filename,
        chart_type,
        xaxis_type,
        yaxis_type,
):
    """アップロードされたデータのグラフを描画"""

    basename = os.path.splitext(filename)[0]
    # ファイル名の1つ目の'_'で区切って、グラフタイトルとY軸名に分ける
    if '_' in basename:
        title, yaxis_name = basename.split('_', 1)
    # ファイル名に'_'がなければグラフタイトル、Y軸名ともにファイル名
    else:
        title, yaxis_name = basename, basename

    def args(i):
        """graph_objs helper func"""
        return {'x': df.index, 'y': df[i], 'name': i}

    # チャートの種類をディクショナリで分岐
    # 内包表記でdfの列の数だけトレース
    data = {
        'Line': [go.Scatter(args(i)) for i in df.columns],
        'Bar': [go.Bar(args(i)) for i in df.columns],
        'Histogram':
        [go.Histogram(x=df[i], name=i, opacity=.5) for i in df.columns],
        'Pie': [
            go.Pie(labels=df.index,
                   values=df[i],
                   name=i,
                   domain={'column': list(df.columns).index(i)})
            for i in df.columns
        ],
        'Polar': [
            go.Scatterpolar(
                r=df[i],
                theta=df.index,
                name=i,
            ) for i in df.columns
        ],
        'Heatmap': [go.Heatmap(x=df.index, y=df.columns, z=df.values)],
        'Box': [go.Box(y=df[i], name=i) for i in df.columns],
        # 'Contour': [go.Contour(x=df.index, y=df.columns, z=df.values)]
        '3D Scatter': [
            go.Scatter3d(x=df.index, y=df.columns, z=df[i], name=i)
            for i in df.columns
        ],
        '3D Surface': [
            go.Surface(x=df.index,
                       y=df.columns,
                       z=df.values,
                       name=yaxis_name,
                       contours=go.surface.Contours(
                           z=go.surface.contours.Z(show=True,
                                                   usecolormap=True,
                                                   highlightcolor="#42f462",
                                                   project=dict(z=True)))),
        ],
        '2D Histogram': [go.Histogram2d(x=df.iloc[:, 0], y=df.iloc[:, 1])]
    }

    # チャートの種類でレイアウトを分岐
    # 分岐にはdefaultdictを使い、デフォルトはlambda式で返す
    layout = defaultdict(
        # default layout
        lambda: go.Layout(title=go.layout.Title(text=title),
                          xaxis={
                              'type': xaxis_type,
                              'title': df.index.name,
                              'rangeslider': dict(visible=False),
                          },
                          yaxis={
                              'type': yaxis_type,
                              'title': yaxis_name,
                          },
                          margin={
                              'l': 60,
                              'b': 50
                          },
                          hovermode='closest'),
        # other layout
        {
            'Histogram':
            go.Layout(title=title,
                      xaxis={'title': 'Value'},
                      yaxis={'title': 'Count'},
                      barmode='overlay',
                      hovermode='closest'),
            'Pie':
            go.Layout(title=go.layout.Title(text=title),
                      grid={
                          'columns': len(df.columns) - 1,
                          'rows': 1
                      },
                      hovermode='closest')
        })
    return dcc.Graph(id='the_graph',
                     figure={
                         'data': data[chart_type],
                         'layout': layout[chart_type]
                     })


def data_table(df):
    """アップロードされたデータの表を描画"""
    df.reset_index(inplace=True)  # indexもテーブルに含めるため
    data = df.to_dict('records')
    columns = [{'name': _i, 'id': _i} for _i in df.columns]
    return dash_table.DataTable(data=data, columns=columns)


def parse_contents(contents, filename, date, chart_type, xaxis_type,
                   yaxis_type):
    content_type, content_string = contents.split(',')

    decoded = base64.b64decode(content_string)
    try:
        if 'csv' in filename:
            # Assume that the user uploaded a CSV file
            df = pd.read_csv(io.StringIO(decoded.decode('utf-8')),
                             index_col=0,
                             parse_dates=True)
        elif 'xls' in filename:
            # Assume that the user uploaded an excel file
            df = pd.read_excel(io.BytesIO(decoded),
                               index_col=0,
                               parse_dates=True)
    except Exception as e:
        print(e)
        return html.Div(['There was an error processing this file.'])

    return html.Div([
        data_graph(df, filename, chart_type, xaxis_type, yaxis_type),
        html.H5(filename),
        html.H6(datetime.datetime.fromtimestamp(date)),
        data_table(df),
        html.Hr(),  # horizontal line

        # For debugging, display the raw contents provided by the web browser
        html.Div('Raw Content'),
        html.Pre(contents[0:200] + '...',
                 style={
                     'whiteSpace': 'pre-wrap',
                     'wordBreak': 'break-all'
                 })
    ])


@app.callback(Output(
    'output-data-upload',
    'children',
), [
    Input('upload-data', 'contents'),
    Input('chart-type', 'value'),
    Input('xaxis-type', 'value'),
    Input('yaxis-type', 'value'),
], [State('upload-data', 'filename'),
    State('upload-data', 'last_modified')])
def update_output(list_of_contents, chart_type, xaxis_type, yaxis_type,
                  list_of_names, list_of_dates):
    if list_of_contents is not None:
        children = [
            parse_contents(c, n, d, chart_type, xaxis_type, yaxis_type)
            for c, n, d in zip(list_of_contents, list_of_names, list_of_dates)
        ]
        return children


if __name__ == '__main__':
    app.run_server(debug=True, host='0.0.0.0', port=8880)
```