Colours and symbolics#

  • Colours, symbols, and flags are powerful elements in dashboards and reports for guiding the user’s attention and conveying messages.

  • These elements are relevant both inside plots and throughout the dashboards as a whole.

Plots#

Groups#

  • Colours, linestyles and plot symbols are typically used to identify or emphasize groups or classes.

  • Most plotting libraries have contrasting colour series specially made for categories.

  • Scatterplots with group-wise symbols or symbol selection based on a DataFrame column.

# Use plotly express to plot petal widths and lengths of the iris data set (imported from plotly). 
# Colour and symbol by species.
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x="petal_width", y="petal_length", color="species", symbol="species")
fig.show()

Focused element#

  • Clicking or hovering over an element can be used to invoke a change.

    • Colour or size change on the selected element.

    • “Defocus” on the remaining elements, e.g., by changing opacity or hue.

import plotly.graph_objects as go

df = px.data.iris()
x = df["sepal_width"]
y = df["sepal_length"]

# Main plot
f = go.FigureWidget([go.Scatter(x=x, y=y, mode='markers')])
f.update_layout(xaxis_title="Sepal width", yaxis_title="Sepal length")
# Set xlimits and ylimits
f.update_yaxes(range=[4, 8.1])
f.update_xaxes(range=[1.8, 4.5])

# Create a color list based on px.colors.qualitative.Vivid which has three colors.
colors = [px.colors.qualitative.Vivid[i] for i in df["species_id"]]
sizes = [10] * len(x)

# Attributes of the scatter object
scatter = f.data[0]
scatter.marker.color = colors
scatter.marker.size = sizes
f.layout.hovermode = 'closest'

# Create our callback function
def update_point(trace, points, selector):
    species = df["species_id"][points.point_inds[0]]
    # Change all ellements in cols that have df.species != species gray
    # to '#bae2be'. Keep in mind that df.species is a series and that cols is a list.
    cols = ['#BBBBBB' if s != species else px.colors.qualitative.Vivid[s] for s in df.species_id]
    size = [14 if s == species else 10 for s in df.species_id]
    
    with f.batch_update():
        scatter.marker.color = cols
        scatter.marker.size = size

def reset_point(trace, points, selector):
    with f.batch_update():
        scatter.marker.color = colors
        scatter.marker.size = sizes

# Assign the callback function to the scatter object
scatter.on_hover(update_point)
scatter.on_unhover(reset_point)

f # Do not use .show() or the figure will not be interactive

Outliers/alerts#

  • The same basic techniques can be used as with focused elements, but the message should be stronger.

  • Where a focused element may obtain a more intense colour or grow slightly, a proper contrast colour, typically bright red, is needed for outliers that warrant an alert.

  • For outliers that are seen as part of the background noise, the symbolics should not be as strong.

df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species")

# Add a red cross behind the samples with lowest and highets sepal width
df_extreme = df[(df.sepal_width == df.sepal_width.min()) | (df.sepal_width == df.sepal_width.max())]
fig.add_trace(go.Scatter(x=df_extreme.sepal_width, y=df_extreme.sepal_length, mode="markers",
                         marker=dict(color="black", size=12, symbol="cross"), showlegend=False))
fig.add_trace(go.Scatter(x=df_extreme.sepal_width, y=df_extreme.sepal_length, 
                         mode="markers", marker=dict(color="red", size=12, symbol="cross-open"), showlegend=False))

# Traces are plotted in the order they are added. To plot the crosses behind the original data, we need to change the order of the traces.
fig.data = (fig.data[3], fig.data[4], fig.data[0], fig.data[1], fig.data[2])

# Add title and axis labels
fig.update_layout(title="Can you spot the outliers?", xaxis_title="Sepal width", yaxis_title="Sepal length")
fig.show()
# Plot a random sample of 1000 normal distributed points as a 2D scatter plot.
import numpy as np
np.random.seed(1)
N = 1000
random_x = np.random.randn(N)
random_y = np.random.randn(N)
dist_origin = np.sqrt(random_x ** 2 + random_y ** 2)

# Let the colour be blue and the opacity 0.7 for points closer than 2 from the origin. Use opacity = 0.2 for all other points.
fig = px.scatter(x=random_x[dist_origin > 2], y=random_y[dist_origin > 2], opacity=0.2, color_discrete_sequence=['blue'])
fig.add_trace(go.Scatter(x=random_x[dist_origin <= 2], y=random_y[dist_origin <= 2], 
                         mode="markers", marker=dict(color="blue", opacity=0.7), showlegend=False))

# Add a circle with radius 2 around the origin
fig.add_shape(type="circle", xref="x", yref="y", x0=-2, y0=-2, x1=2, y1=2, line_color="LightSeaGreen")
fig.update_layout(title="Softening outliers using opacity")
fig.show()

Colour blindness#

  • Colour blindness affects up to 8% of men and 0.5% of women.

  • Red-green is the most common problem, followed by blue-yellow, but there are many variants.

    • Each version comes with several confusions, e.g., red-green colourblindness may cause problems in distingushing:

      • cyan and grey,

      • rose-pink and grey,

      • blue and purple,

      • yellow and neon green,

      • red, green, orange, brown.

  • Some sequential colourmaps are designed to convey the differences regardless of vision type by superimposing a light-to-dark scale and yellow-to-blue scale, e.g., Cividis, Viridis, and Parula.

A simplified depiction of colour perception for various conditions (public domain figure from Wikimedia Commons)
https://github.com/khliland/IND320/blob/main/D2Dbook/images/Color_blindness.png?raw=TRUE

# Use plotly express to plot petal widths and lengths of the iris data set (imported from plotly). Colour by species. 
# Use a colour scale friendly to colour blind people.
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x="petal_width", y="petal_length", color="species", color_discrete_sequence=px.colors.qualitative.Vivid)
fig.show()

Colour perception#

  • Colour perception is not linear with RGB values.

  • Thus, resolving colours of different intensities is not equal for all colours.

  • Some colour gradients are specially made to appear linear.

# Add ipywidgets slider to control the number of rectangles.
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import ipywidgets as widgets
def plot_rectangles(n):
    # Set figure size to 10 x 4
    _, ax = plt.subplots(figsize=(10, 4))
    for i in range(n):
        rectR = patches.Rectangle((i / n, 5), 1 / n, 1, facecolor=(i / (n-1), 0, 0))
        ax.add_patch(rectR)
        rectG = patches.Rectangle((i / n, 4), 1 / n, 1, facecolor=(0, i / (n-1), 0))
        ax.add_patch(rectG)
        rectB = patches.Rectangle((i / n, 3), 1 / n, 1, facecolor=(0, 0, i / (n-1)))
        ax.add_patch(rectB)
        rectY = patches.Rectangle((i / n, 2), 1 / n, 1, facecolor=(i / (n-1), i / (n-1), 0))
        ax.add_patch(rectY)
        rectC = patches.Rectangle((i / n, 1), 1 / n, 1, facecolor=(0, i / (n-1), i / (n-1)))
        ax.add_patch(rectC)
        rectM = patches.Rectangle((i / n, 0), 1 / n, 1, facecolor=(i / (n-1), 0, i / (n-1)))
        ax.add_patch(rectM)
    # Set y limits
    ax.set_ylim(0, 6)
    plt.show()
widgets.interact(plot_rectangles, n=widgets.IntSlider(min=20, max=100, step=1, value=20))
<function __main__.plot_rectangles(n)>
# There are many colourmaps in matplotlib.
from matplotlib import colormaps
list(colormaps)
['magma',
 'inferno',
 'plasma',
 'viridis',
 'cividis',
 'twilight',
 'twilight_shifted',
 'turbo',
 'Blues',
 'BrBG',
 'BuGn',
 'BuPu',
 'CMRmap',
 'GnBu',
 'Greens',
 'Greys',
 'OrRd',
 'Oranges',
 'PRGn',
 'PiYG',
 'PuBu',
 'PuBuGn',
 'PuOr',
 'PuRd',
 'Purples',
 'RdBu',
 'RdGy',
 'RdPu',
 'RdYlBu',
 'RdYlGn',
 'Reds',
 'Spectral',
 'Wistia',
 'YlGn',
 'YlGnBu',
 'YlOrBr',
 'YlOrRd',
 'afmhot',
 'autumn',
 'binary',
 'bone',
 'brg',
 'bwr',
 'cool',
 'coolwarm',
 'copper',
 'cubehelix',
 'flag',
 'gist_earth',
 'gist_gray',
 'gist_heat',
 'gist_ncar',
 'gist_rainbow',
 'gist_stern',
 'gist_yarg',
 'gnuplot',
 'gnuplot2',
 'gray',
 'hot',
 'hsv',
 'jet',
 'nipy_spectral',
 'ocean',
 'pink',
 'prism',
 'rainbow',
 'seismic',
 'spring',
 'summer',
 'terrain',
 'winter',
 'Accent',
 'Dark2',
 'Paired',
 'Pastel1',
 'Pastel2',
 'Set1',
 'Set2',
 'Set3',
 'tab10',
 'tab20',
 'tab20b',
 'tab20c',
 'grey',
 'gist_grey',
 'gist_yerg',
 'Grays',
 'magma_r',
 'inferno_r',
 'plasma_r',
 'viridis_r',
 'cividis_r',
 'twilight_r',
 'twilight_shifted_r',
 'turbo_r',
 'Blues_r',
 'BrBG_r',
 'BuGn_r',
 'BuPu_r',
 'CMRmap_r',
 'GnBu_r',
 'Greens_r',
 'Greys_r',
 'OrRd_r',
 'Oranges_r',
 'PRGn_r',
 'PiYG_r',
 'PuBu_r',
 'PuBuGn_r',
 'PuOr_r',
 'PuRd_r',
 'Purples_r',
 'RdBu_r',
 'RdGy_r',
 'RdPu_r',
 'RdYlBu_r',
 'RdYlGn_r',
 'Reds_r',
 'Spectral_r',
 'Wistia_r',
 'YlGn_r',
 'YlGnBu_r',
 'YlOrBr_r',
 'YlOrRd_r',
 'afmhot_r',
 'autumn_r',
 'binary_r',
 'bone_r',
 'brg_r',
 'bwr_r',
 'cool_r',
 'coolwarm_r',
 'copper_r',
 'cubehelix_r',
 'flag_r',
 'gist_earth_r',
 'gist_gray_r',
 'gist_heat_r',
 'gist_ncar_r',
 'gist_rainbow_r',
 'gist_stern_r',
 'gist_yarg_r',
 'gnuplot_r',
 'gnuplot2_r',
 'gray_r',
 'hot_r',
 'hsv_r',
 'jet_r',
 'nipy_spectral_r',
 'ocean_r',
 'pink_r',
 'prism_r',
 'rainbow_r',
 'seismic_r',
 'spring_r',
 'summer_r',
 'terrain_r',
 'winter_r',
 'Accent_r',
 'Dark2_r',
 'Paired_r',
 'Pastel1_r',
 'Pastel2_r',
 'Set1_r',
 'Set2_r',
 'Set3_r',
 'tab10_r',
 'tab20_r',
 'tab20b_r',
 'tab20c_r']

Dashboards and reports#

Colours#

  • Colours and symbols are also useful outside plots.

  • Visual cues:

    • Grouping elements with separate background colour or border.

    • Making some plot stand out from the crowd by using a contrast around it.

    • Changing background, text colour, or text background colour based on user actions or events in the data.

  • Colour themes can be complimentary to plots or crash harshly. Both are effects that are useful.

    • Writer’s colour-picker is useful for syncing colours or finding different versions.

    • Changing between HEX (#000000), RGB (0,0,0), and HSL (0,0,0) can be used when exploring colours:

      • For instance, choose a colour using the colour picker (Writer can pick anything visible on the screen), swithc to HSL and cycle through hues.

      • This results in limiting a colour search to colours with matching saturation (colour intensity from gray to pure) and lightness (from black to full colour/white).

    • Color wheels are also useful tools for selecting matching colours.

  • Colours for KPIs (key performance indicators) emphasise ranges, e.g., positive vs negative, within the normal vs extreme, etc.

  • Colours for alerts are powerful, not only in symbols/signs, but also for backgrounds.

Symbols#

  • Emojis, cliparts, animated GIFs and similar are effective in catching attention.

  • Therefore they must be used sparingly.

    • A spinning company logo might be cool at first sight, but quickly becomes anoying.

    • Static, graphical elements that blend in to the theme may be used as long as they have a function, e.g., conveying identity, showing a state, or making it easy to detect which app/environment is active.

  • Symbols used as alerts must be immediately visible using sharp contrasts.

    • In Writer, an alert sign can be loaded on startup, but set to invisible or it can be a plot that changes.

Exercise#

  • Test the use of colors and conditional symbols/flags in Writer.