I just posted a PR with minimal internal code changes to fix this fix: handle 'node_color' in element transformations for Bokeh and MPL by epaaso · Pull Request #6678 · holoviz/holoviews · GitHub.
With this fix, if you define a Dataset
instead of a dataframe for the nodes
and use node_fill_color
instead of node_color
in bokeh
, you can get a correct color assignment of the nodes.
Here is a minimal script that correctly assigns color to the nodes:
import pandas as pd
import holoviews as hv
# --- Choose backend ('matplotlib' or 'bokeh') ---
# hv.extension('matplotlib')
hv.extension('bokeh')
# --- Category palette (>=9 categories) ---
# Using distinct colors (Tableau/ColorBrewer inspired) for clarity
palette = {
'A': '#1f77b4', # blue
'B': '#ff7f0e', # orange
'C': '#2ca02c', # green
'D': '#d62728', # red
'E': '#9467bd', # purple
'F': '#8c564b', # brown
'G': '#e377c2', # pink
'H': '#7f7f7f', # gray
'I': '#17becf', # cyan
}
# --- Build minimal links (already aggregated) ---
# Pattern: sources suffixed with (A), targets with (B) to mimic prior pattern
# We'll create a chain A->B->C->... plus a few cross links.
chain_letters = list(palette.keys())
links = []
for i in range(len(chain_letters) - 1):
s = chain_letters[i]
t = chain_letters[i+1]
links.append({
'source': f'{s} (A)',
'target': f'{t} (B)',
'value': 5 + i, # simple increasing values
'color': palette[s] # color taken from source category
})
# Add a few cross links to make it more interesting
cross_specs = [
('A', 'D', 2),
('B', 'E', 3),
('C', 'G', 1),
('E', 'I', 4),
('F', 'H', 2),
]
for s, t, v in cross_specs:
links.append({
'source': f'{s} (A)',
'target': f'{t} (B)',
'value': v,
'color': palette[s]
})
links_df = pd.DataFrame(links)
# --- Derive nodes_df from link endpoints ---
labels = pd.unique(pd.concat([links_df.source, links_df.target]))
nodes_df = pd.DataFrame(
{
'color': [palette[l.split(' (', 1)[0]] for l in labels],
'label': labels
},
index=labels
)
nodes_df.index.name = 'index'
from holoviews import Dataset
nodes_ds = Dataset(nodes_df, kdims=['label'], vdims=['color'])
# --- Construct Sankey ---
sk = hv.Sankey((links_df, nodes_ds), kdims=['source', 'target'], vdims=['value', 'color'])
if hv.Store.current_backend == 'bokeh':
sk = sk.opts(
title='Minimal Sankey (bokeh)',
labels='label',
edge_color='color',
node_fill_color='color',
width=500, height=350,
hooks=[label_hook()],
bgcolor='white'
)
else: # matplotlib
sk = sk.opts(
title='Minimal Sankey (matplotlib)',
labels='label',
edge_color='color', # constant or map; here field -> edges (length = edges)
node_color='color', # taken from node vdims (length = nodes)
fig_inches=500/150,
aspect=350/500,
hooks=[label_hook()],
bgcolor='white'
)
# --- Display (in a notebook just evaluate `sk`) ---
if __name__ == '__main__':
# If run as a script, export to static (matplotlib) or show (bokeh)
if hv.Store.current_backend == 'matplotlib':
hv.save(sk, 'sankey_minimal.png', fmt='png')
print("Saved sankey_minimal.png")
else:
from bokeh.io import output_file, save
hv.renderer('bokeh')
output_file('sankey_minimal.html')
save(hv.render(sk))
print("Saved sankey_minimal.html")