おかざきPの記録

Mac, QNAPの設定やプロデューサー業の記録

Pythonで測定装置のcsvファイルをグラフ化するアプリを作る(3. グラフ調整編)

 前々回からの続きものです.こんなのの作り方を公開していきます. drive.google.com

 今回はグラフのx, y軸などを調整する部分を作成します.

目標

 下図の赤枠部分を作成します.

今回作る部分

開発環境

 初回から追加ありません.Pythonpandasdashをインストールして下さい.

グラフの設定項目の作成

軸や色分けなどを選択できるプルダウンを配置する

 x, y, z, 色, 点や線の種類, 列, 行, 各点のラベル, マウスオーバー時の表示項目をプルダウンで選択できるようにします. これらは項目名が違うだけで中身はほぼ同じです.従ってこのようなパーツを生成するクラスというか関数群をまず作ります.

class Components(object):
    """グラフの設定項目を選択するパーツを生成します.
    Attribute
    ----------
    name: str
        plotly.expressの関数に渡すキーワード引数の名前.

    Methods
    ----------
    make_dropdown(multi): 設定項目を選択するためのプルダウンを生成します.
    make_button(): 高度な設定を表示するためのボタンを生成します.
    make_range(): 数字の上下限を設定するための入力ボックスを生成します.
    make_toggle(): 軸の常用対数表示を切り替えるスイッチを生成します.
    make_errorbar(): エラーバーの範囲を設定するための入力ボックスを生成します.ただし動作未確認.
    make_collapse(): ボタンで表示と非表示が切り替わる高度な設定を生成します.
    """
    def __init__(self, name):
        self.name = name

    def make_dropdown(self, multi=False):
        return dbc.Row(
    [dbc.Col(f'{self.name}'),
     dbc.Col(
         dcc.Dropdown(
             options=[], value=None, multi=multi, id=f'graph-{self.name}',
             className='dbc', style={'width': '15vw', 'float': 'right'},
            )
         ),
    ],
    )

    def make_button(self):
        return dbc.Button(
          'advanced settings', n_clicks=0,
          id=f'{self.name}-button', size='sm', style={'width': '7vw'},
        )

    def make_range(self):
        return [
           html.Div('Range', style={'text-align': 'center'}),
           dbc.InputGroup(
               [dbc.Input(
                    placeholder='min', id=f'{self.name}min', type='number',
                    ),
                dbc.InputGroupText('–'),
                dbc.Input(
                    placeholder='max', id=f'{self.name}max', type='number',
                    ),
               ],
               ),
        ]

    def make_toggle(self):
        return daq.BooleanSwitch(
                on=False, id=f'graph-log_{self.name}',
                label=f'log {self.name}', labelPosition='top',
                )

    def make_errorbar(self):
        return [
           html.Div(
                'Error bar', style={'text-align': 'center'},
               ),
           dbc.InputGroup(
               [dbc.Input(
                    placeholder='None or lower limit',
                    id=f'error_{self.name}_minus', type='number',
                    ),
                dbc.InputGroupText('–'),
                dbc.Input(
                    placeholder='size or upper limit',
                    id=f'error_{self.name}', type='number',
                    ),
               ],
               ),
        ]

    def make_collapse(self):
        button = self.make_button()
        options = dbc.Row(
                        [dbc.Col(self.make_range()), dbc.Col(self.make_toggle())])
        collapse = dbc.Collapse(
                        options, is_open=False, id=f'{self.name}_collapse')
        return dbc.Col(
            [button, collapse],
            width={'offset': 1}, style={'padding-bottom': '10pt'},
        )


class Axis(Components):
    """高度な設定を含む軸を選択します.
    Method
    ----------
    make_component(multi): 軸を選択するドロップダウンと高度な設定を行うボタンを生成します.
    """
    def make_component(self, multi=False):
        components = [self.make_dropdown(multi), self.make_collapse()]
        return html.Div(components)

 これを使ってグラフの設定項目を選択する部分を作ってcontainerに反映します.

graph_settings = [
    html.H5(['Settings']),
    'Graph type',# 後で中身を作ります
    Axis('x').make_component(multi=True),# 複数選択できるようにした理由は後で散布図行列を使うためです
    Axis('y').make_component(multi=False),
    Axis('z').make_component(multi=False),
    Components('color').make_dropdown(),
    Components('symbol').make_dropdown(),
    Components('facet_col').make_dropdown(),
    Components('facet_row').make_dropdown(),
    Components('text').make_dropdown(),
    Components('hover_name').make_dropdown(),
    dbc.Row(
        [dbc.Col(
             dbc.InputGroup(
                 [dbc.InputGroupText('width'),
                  dbc.Input(value=600, type='number', id='graph-width'),
                  dbc.InputGroupText('pixel')
                 ]
                 )
             ),
         dbc.Col(
             dbc.InputGroup(
                 [dbc.InputGroupText('height'),
                  dbc.Input(type='number', value=400, id='graph-height'),
                  dbc.InputGroupText('pixel')
                 ],
                 )
             )
        ],
        ),
]

container = [
    dbc.Row(
        [dbc.Col(graph_settings, width=3, class_name='bg-secondary',
                 style={'height': '60vh', 'overflow': 'scroll'},
            ),
         dbc.Col(
             graph, width=9, style={'height': '60vh', 'overflow': 'scroll'}),
        ],
        ),
    dbc.Row(
        [dbc.Col(table_info, width=8, class_name='bg-info text-black'),
         dbc.Col(table_detail, width=4, class_name='bg-primary text-black'),
        ], style={'height': '40vh'},
        ),
]

プルダウンの設置

表の列名をプルダウンに自動で反映させる

 作成したプルダウンは中身が空でまだ何も選択できません.ここからグラフに何を表示するか選べるようにするには,csvの表から列名を抽出する必要があります. この操作は表が更新された時に実行されるようにします.

dropdowns = ['x', 'y', 'z', 'color', 'symbol',
             'facet_col', 'facet_row', 'text', 'hover_name'
            ]

for arg in dropdowns:
    # なぜか上書きされずに全てのドロップダウンの設定に成功する
    @app.callback(
            Output(f'graph-{arg}', 'options'),
            Input('table_info', 'columns'),
            Input('table_detail', 'columns'),
            prevent_initial_call=True
        )
    def update_dropdown(col_info, col_detail):
        if col_info is None:
            col_info = []
        if col_detail is None:
            col_detail = []
        col = ['_'.join(c['name']) if c['name'][0]!='' else c['name'][1]
                for c in col_info
              ]
        col += [c['name'] for c in col_detail]
        return list(set(col))

ドロップダウンで選択したパラメータをグラフに反映させる

 前回作成したグラフ描画用の関数を書き換えます.

dropdowns = ['x', 'y', 'z', 'color', 'symbol',
             'facet_col', 'facet_row', 'text', 'hover_name'
            ]

@app.callback(
    Output('graph', 'figure'),
    inputs={
        'data_info': Input('table_info', 'derived_virtual_data'),
        'data_detail': Input('table_detail', 'derived_virtual_data'),
        'graph_kws': {# グラフの設定項目をまとめて渡す
            key: Input(f'graph-{key}', 'value')
                for key in dropdowns + ['width', 'height']
            },
        },
    prevent_initial_call=True,
)
def update_graph(
        data_info, data_detail, graph_kws,
    ):
    if len(data_info)==0:
        return go.Figure()
    df = pd.DataFrame(data_info)
    if len(data_detail)!=0:
        df = df.join(pd.DataFrame(data_detail).set_index('Filename'),
                     on='Filename', how='inner'
              ).reset_index()
    graph_kws.pop('z')# 散布図にz軸はないため削除しないとエラー発生
    try:
        graph_kws['x'] = graph_kws['x'][0]# 複数選択できるドロップダウンの値はリストになっているため
        fig = px.scatter(df, **graph_kws)
    except:
        fig = go.Figure()
    return fig

 ここまで作ってプルダウンからの選択やwidth, heightを入力すると下図のようなグラフが描画できます.

プルダウンから選択してグラフ化した様子

軸の高度な設定の反映

 ボタンだけ作ってあった高度な設定の中身を作ります.まず設定を開閉できるようにします.

for btn in list('xyz'):
    @app.callback(
            Output(f'{btn}_collapse', 'is_open'),
            Input(f'{btn}-button', 'n_clicks'),
            State(f'{btn}_collapse', 'is_open'),
            prevent_initial_call=True
        )
    def open_collapse(clicks, is_open):
        return not is_open

高度な設定を開いた状態
 グラフ描画関数もこの設定を受け取れるように書き換えます.

@app.callback(
    Output('graph', 'figure'),
    inputs={
        'data_info': Input('table_info', 'derived_virtual_data'),
        'data_detail': Input('table_detail', 'derived_virtual_data'),
        'graph_kws': {# グラフの設定項目をまとめて渡す
            key: Input(f'graph-{key}', 'value')
                for key in dropdowns + ['width', 'height']
            },
        'log_axis': {f'log_{x}': Input(f'graph-log_{x}', 'on') for x in 'xyz'},
        'axis_ranges': {
            f'{key}': Input(f'{key}', 'value')
                for key in [f'{x}min' for x in 'xyz']+[f'{x}max' for x in 'xyz']
            },
        },
    prevent_initial_call=True,
)
def update_graph(
        data_info, data_detail, graph_kws, axis_ranges, log_axis,
    ):
    if len(data_info)==0:
        return go.Figure()
    df = pd.DataFrame(data_info)
    if len(data_detail)!=0:
        df = df.join(pd.DataFrame(data_detail).set_index('Filename'),
                     on='Filename', how='inner'
              ).reset_index()
    graph_kws.pop('z')

    # 高度な設定をキーワード引数に変換する
    for key in 'xyz':
        if key not in graph_kws.keys():
            continue
        keymin = axis_ranges[f'{key}min']
        keymax = axis_ranges[f'{key}max']
        if (keymin is not None
            and keymax is not None
            ):
            graph_kws[f'range_{key}'] = [
                    keymin, keymax
                ]
        graph_kws[f'log_{key}'] = log_axis[f'log_{key}']

    try:
        graph_kws['x'] = graph_kws['x'][0]
        fig = px.scatter(df, **graph_kws)
    except:
        fig = go.Figure()
    return fig

y軸の範囲を設定して常用対数表示に変更したグラフ

次回

 散布図以外のグラフも作れるようにします.
 投稿しました.