1
votes

I am building a Dash app and attempting to utilize Plotly's category_order and color_discrete_sequence to assign a specific color to unique string values that reside in a dataframe column. These parameters seem to be described as taking non-numerical data (e.g. strings) and assigning them colors in an order the user specifies. However, the code doesn't seem to take and I only get monochromatic black dots for all datapoints (image below). Hoping someone can see what I'm missing and offer guidance how to use these parameters, or another process, to get the intended result.

Unintended monochromatic marker color demo

My process is as follows:

I have a dataframe containing the columns "band," "lat," and "lon," where lat and lon will specify the location of a point in a scattermapbox plot and band takes on a list of values such as:

data = {'band':['10m','10m','6m','2m','2m','2m','1.25m','70cm','33cm','33cm','33cm','23cm'],
              'Lat': ['35.5','34.2','35.9','36.1','35.2','36.2','33.9','36.4','35.1','34.9','32.9','35.0'],
              'Lon': ['-78.5','-77.3','-79.0','-78.6','-79.3','-77.0','-78.5','-77.7','-79.9','-78.8','-79.1','-79.0']}
df = pd.DataFrame.from_dict(data)

This mapplot will need its colors to correspond with other charts, so I want to use category_order as the documentation reads...

By default, in Python 3.6+, the order of categorical values in axes, legends and facets depends on the order in which these values are first encountered in data_frame (and no order is guaranteed by default in Python below 3.6). This parameter is used to force a specific ordering of values per column. The keys of this dict should correspond to column names, and the values should be lists of strings corresponding to the specific display order desired."

band_categories = {'band':['10m', '6m', '2m', '1.25m', '70cm', '33cm', '23cm']}

is set to establish the order I wish the scattermapbox to use.

Lastly, color_discrete_sequence works directly with category_order by definition...

Strings should define valid CSS-colors. When color is set and the values in the corresponding column are not numeric, values in that column are assigned colors by cycling through color_discrete_sequence in the order described in category_orders, unless the value of color is a key in color_discrete_map.

so it receives my desired color order band_colors_list = ['green', 'red', 'blue', 'orange', 'purple', 'gray', 'yellow']

The final marker dict is compiled and is assigned to the scattermapbox dict and pushed to the Dash element.

import pandas as pd
import numpy as np
from pandas.api.types import CategoricalDtype
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

data = {'band':['10m','10m','6m','2m','2m','2m','1.25m','70cm','33cm','33cm','33cm','23cm'],
              'Lat': ['35.5','34.2','35.9','36.1','35.2','36.2','33.9','36.4','35.1','34.9','32.9','35.0'],
              'Lon': ['-78.5','-77.3','-79.0','-78.6','-79.3','-77.0','-78.5','-77.7','-79.9','-78.8','-79.1','-79.0']}
df = pd.DataFrame.from_dict(data)
df.head()
token = drop_mapbox_token_string_here
layers = []
band_categories = ['10m', '6m', '2m', '1.25m', '70cm', '33cm', '23cm']
band_colors_list = ['green', 'red', 'blue', 'orange', 'purple', 'gray', 'yellow']
app = dash.Dash(__name__)

template = {'layout': {'paper_bgcolor': "#f3f3f1", 'plot_bgcolor': "#f3f3f1"}}

def blank_fig(height):
    """
    Build blank figure with the requested height
    """
    return {
        'data': [],
        'layout': {
            'height': height,
            'template': template,
            'xaxis': {'visible': False},
            'yaxis': {'visible': False},
        }
    }

# Build Dash layout
app.layout = html.Div(children=[
    html.Div(children=[
        dcc.Graph(
            id='map-graph',
            figure=blank_fig(500),
            config={'displayModeBar': False},
        ),],
        id="map-div"
    )
])

@app.callback(
    Output('map-graph', 'figure'),
    [Input('map-graph', 'relayoutData')])
def update_plots(relayout_data):

    marker = {
        'color': df.band,
        'category_order': band_categories,
        'color_discrete_sequence': band_colors_list,
        'size': 5,
        'opacity': 0.6
    }
    map_graph = {'data': [{
        'type': 'scattermapbox',
        'lat': df.Lat, 'lon': df.Lon,
        'marker': marker
        }],
        'layout': {
            'template': template,
            'uirevision':True,
            'mapbox':{
                'style': "light",
                'accesstoken': token,
                'layers': layers,
            },
            'margin': {"r": 0, "t": 0, "l": 0, "b": 0},
            'height': 500
        },
    }
    map_graph['layout']['mapbox'].update()

    return (map_graph)

app.scripts.config.serve_locally = True
app.css.config.serve_locally = True
app.run_server(debug=True, use_reloader=False)
1
Pleaes provide a runnable snippet if you can. Don't worry about the entire Dash app. Just the figure you're building. - vestland
Apologies. Above should be the minimal amount of code needed for the single figure, save actually starting the Dash app. - Prometheus2508
@Prometheys2508 Indentations after def don't look right. And you're missing your imports - vestland
Should be fixed - Prometheus2508
Note, you'll need to pick up a mapbox token: docs.mapbox.com/help/how-mapbox-works/access-tokens - Prometheus2508

1 Answers

2
votes

Solved the issue.

Ultimately, I was getting mixed up with components in Plotly Express and various scatter map components in Plotly Graph Objects. While category_order and discrete_color_sequence are resident in Plotly Express scatters, they are not in Plotly Graph Object's Scattermapbox.

While one avenue would be to convert to using one of these other components that allow for discrete color definitions, I instead went with a less invasive approach. Simply define a new dataframe column that has preset colors as a function of the band column and push this column into the color parameter under marker.

band_categories = ['10m', '6m', '2m', '1.25m', '70cm', '33cm', '23cm']
band_colors_list = ['green', 'red', 'blue', 'orange', 'purple', 'gray', 'yellow']
band_dict = dict(zip(band_categories,band_colors_list)) #set up band to color substitution dict
df['color'] = df['band'].replace(to_replace=band_colorscale)

and later...

marker = {
    'color': df.color,
    'size': 5,
    'opacity': 0.6
}

Arguably less elegant and memory intensive, since we're storing extra attributes for a large dataset, but it works.