Making a Figure Editor in Dash?

I’m using Plotly offline, which means that I cannot access chart studios to edit plots on the fly, so I’ve decided to start making one that will work offline with Dash. I’m currently unable to figure out the correct callbacks in order to achieve what I want. Below will attempt to explain my thinking, and I will provide some source code.

  1. Make a Figure in a Python script
  2. Save the figure as a json file to some location
  3. [if the user wants to edit this plot later they will use this tool]
  4. Open the figure.json file or drag it into the Dash app
  5. Populate different drop-downs pertaining to that specific figure.
  6. Update the figure based on user selection from drop-downs and fields etc.

import plotly.graph_objects as go
import dash
import dash_core_components as dcc
import dash_html_components as html
import json
import base64
from dash.dependencies import Input, Output, State

def make_dropdown(idname,values,label,**kwargs):
    default = kwargs.get('default','')
    return html.Label([
        label, 
        dcc.Dropdown(id=idname,
                      options=[{'label': i, 'value':i} for i in values],
                      value = default
                      )
        ])


def add_file_select(idname,ftype):
    return dcc.Upload(
            id=idname,
            children=html.Div([
                'Drag and Drop or ',
                html.A(html.Button(f'Select {ftype} File'))
                ]),
            style={
                'width': '100%',
                'height': '60px',
                'lineHeight': '60px',
                'borderWidth': '1px',
                'borderStyle': 'dashed',
                'borderRadius': '5px',
                'textAlign': 'center'
            })

app = dash.Dash(__name__)

app.layout = html.Div(children=[
    add_file_select('uploaded-figure','Plotly Figure'),
    make_dropdown('Lines',[],'Lines'),
    make_dropdown('Colors',[],'Colors'),
    dcc.Graph(id='edit-graph')
    ],
    style={})
         

@app.callback(Output('edit-graph', 'figure'),
            [
                Input('uploaded-figure', 'contents'),
                Input('uploaded-figure', 'filename'),
                Input('Lines','value'),
                Input('Colors','value'),
            ],
            state=[State('edit-graph','figure')])
def update_edit_figure(contents, filename,name,colorvalue, fig):
    ctx = dash.callback_context
    if ctx.triggered:
        ctxtid = ctx.triggered[0]['prop_id'].split('.')[0]
    else:
        ctxtid = ''
    print(ctxtid)
    if ctxtid == 'uploaded-figure':
        try:
            content_type, content_string = contents.split(',')
            decoded = base64.b64decode(content_string).decode('utf-8')
            fig = go.Figure(json.loads(decoded))
            content = fig.to_dict()
        except:
            content = {}
        return content
    elif ctxtid == 'Colors':
        if colorvalue:
            if fig:
                fig.update_traces(marker=dict(color=colorvalue,selector={'name':name}))
                return fig
        return {}
    elif ctxtid == 'Line':
        if fig:
            return fig
        return {}
    else:
        return {}
    

@app.callback([Output('Lines','options'),
               Output('Lines','value')],
              [Input('edit-graph','figure')])
def update_lines(fig):
    if fig:
        names = [{'label':i['name'],'value':i['name']} for i in fig['data']]
        value = content['data'][0]['name']
    else:
        names = []
        value = ''
    return names,value

@app.callback([Output('Colors','options'),
               Output('Colors','value')],
              [Input('edit-graph','figure'),
               Input('Lines','value')
               ])
def update_properties(fig,value):
    d = {}
    color = []
    color_val = ''
    if fig and 'data' in fig:
        for line in fig['data']:
            if line['name'] == value:
                d = line
                break
        if d:
            colors = ['red','blue','yellow','orange']
            color = [{'label':color,'value':color} for color in colors]
    return color,color_val



if __name__ == '__main__':
    app.run_server(debug=True)

I keep getting circular dependencies no matter what I try to do. Any ideas on how to combat that?

Also, sorry i cannot seem to get the imports formatted correctly for the code.

Hi,
if you are ok with not using Dash, you can take a look at the jupyter-lab chart editor extension. It is basically chart editor, but offline.

1 Like

Hi Batalex,

Great Suggestion!

I did not know that this existed, however, I will need to make sure that it will suite my needs before I mark this as a viable solution for myself. But just looking at it, it might be a viable solution for others. Good work! I will update this post after I’ve done some testing.

Unfortunately I could not get this to install on my work machine [I was able to get it to install on my personal laptop]. It looks as though it has too many dependencies that it is trying to grab. But it is very cool, and might serve as a template for my project.