Colored calendar heatmap in dash

Hi all,

I found a interesting github-like activity tracker chart, it is implemented using matplotlib.
https://pythonhosted.org/calmap

my question is: can i embed the calmap chart into my dash app?

Is your matplotlib chart technically just a picture? If so this might help:

1 Like

No. I would like to embed a real interactive chart. I guess we can create one using plotly heatmap but it is not trivial for a beginner like me :slightly_smiling_face:

I am currently working on using a plotly heatmap as a calendar to select/deselect days of production in some analysis I am doing for my company.
I will return when I have a rough idea about how to make a good calendar with plotly heatmap…
So far I have something like the following, but I have yet to test how well it works. :slightly_smiling_face:

year = datetime.datetime.now().year

d1 = datetime.date(year, 1, 1)
d2 = datetime.date(year, 12, 31)

delta = d2 - d1

dates_in_year = [d1 + datetime.timedelta(i) for i in range(delta.days+1)]
weekdays_in_year = [i.weekday() for i in dates_in_year]
weeknumber_of_dates = [int(i.strftime("%V")) for i in dates_in_year]
z = [0]*len(dates_in_year)
text = [str(i) for i in dates_in_year]

data = [
	go.Heatmap(
		x = weekdays_in_year,
		y = weeknumber_of_dates,
		z = z
	)
]
layout = go.Layout(
	title='test'
)

fig = go.Figure(data=data, layout=layout)

I have looked alot at the Github Commits per Day example in https://plot.ly/python/heatmaps/ to get where I am right now, if you need some inspiration.

2 Likes

Okay, so now I know I have something which works… I will add a few comments if you need 'em.
It creates a calendar with weeks along the y-axis and weekdays along the x-axis. When you hover a “cell” the hoverinfo is a string with the given date… :man_technologist::metal:
(Yes, I am too lazy to translate my danish strings into english…)

def holidays():
	year = datetime.datetime.now().year

	d1 = datetime.date(year, 1, 1)
	d2 = datetime.date(year, 12, 31)

	delta = d2 - d1

	dates_in_year = [d1 + datetime.timedelta(i) for i in range(delta.days+1)] #gives me a list with datetimes for each day a year
	weekdays_in_year = [i.weekday() for i in dates_in_year] #gives [0,1,2,3,4,5,6,0,1,2,3,4,5,6,...] (ticktext in xaxis dict translates this to weekdays
	weeknumber_of_dates = [int(i.strftime("%V")) for i in dates_in_year] #gives [1,1,1,1,1,1,1,2,2,2,2,2,2,2,...] name is self-explanatory
	z = np.random.uniform(low=0.0, high=1.0, size=(len(dates_in_year,))) #random numbers to give some mad colorz
	text = [str(i) for i in dates_in_year] #gives something like list of strings like '2018-01-25' for each date. Used in data trace to make good hovertext.

	data = [
		go.Heatmap(
			x = weekdays_in_year,
			y = weeknumber_of_dates,
			z = data_days,
			text=text,
			hoverinfo="text",
			xgap=3, # this
			ygap=3, # and this is used to make the grid-like apperance
			showscale=False
		)
	]
	layout = go.Layout(
		title='Ferielukket Kalender',
		height=1000,
		xaxis=dict(
			showline=True, #draw axis option (maybe I should remove this?)
			tickmode="array",
			ticktext=["Man", "Tirs", "Ons", "Tors", "Fre", "Lør", "Søn"],
			tickvals=[0,1,2,3,4,5,6],
			title="Ugedag"
		),
		yaxis=dict(
			showline=True,
			title="Uge Nr."
		),
		plot_bgcolor=('rgb(0,0,0)') #making grid appear black
	)

	fig = go.Figure(data=data, layout=layout)
	return fig
3 Likes

awesome! it is pretty close to what i need, i will give it a try. may be need to tune the grid a bit so that it become a square grid with white border line.

1 Like

To tune the grid, I think you need to do something smart™ with ticks, I will pass on this one. :sweat_smile:
If you want a white border you just need to change the plot_bgcolor option in the go.Layout

plot_bgcolor=('rgb(0,0,0)') #making grid appear black
plot_bgcolor=('rgb(255,255,255)') #making grid appear white
1 Like

Looks good! Just to add some ideas to help refine the layout a little more. You can:

remove all plot/grid lines:

xaxis = dict(
   showline = False,
   showgrid = False,
   zeroline = False
)
yaxis = dict(
   showline = False,
   showgrid = False,
   zeroline = False
)

set the global font color:

font = dict(color = 'rgb(255, 255, 255)')

and you can use margins to make it skinny:

margin = dict(l=80,r=80,t=400,b=300)
1 Like

this is the changes i make it more github-chart -look-alike (there is still some flaw with the grid dimension which has dependency on the period of my chart.) i wish i could remove the ‘-’ from the axis label to make the chart neater, but i couldn’t find a way.

code:

import datetime
import plotly.graph_objs as go
import numpy as np
import dash_core_components as dcc
import dash_html_components as html
import dash

def holidays():
year = datetime.datetime.now().year

d1 = datetime.date(year, 8, 1)
d2 = datetime.date(year+1, 7, 15)

delta = d2 - d1

dates_in_year = [d1 + datetime.timedelta(i) for i in range(delta.days+1)] #gives me a list with datetimes for each day a year
weekdays_in_year = [i.weekday() for i in dates_in_year] #gives [0,1,2,3,4,5,6,0,1,2,3,4,5,6,…] (ticktext in xaxis dict translates this to weekdays
weeknumber_of_dates = [i.strftime(“%Gww%V”)[2:] for i in dates_in_year] #gives [1,1,1,1,1,1,1,2,2,2,2,2,2,2,…] name is self-explanatory
z = np.random.randint(2, size=(len(dates_in_year)))
text = [str(i) for i in dates_in_year] #gives something like list of strings like ‘2018-01-25’ for each date. Used in data trace to make good hovertext.
#4cc417 green #347c17 dark green
colorscale=[[False, ‘#eeeeee’], [True, ‘#76cf63’]]

data = [
go.Heatmap(
x = weeknumber_of_dates,
y = weekdays_in_year,
z = z,
text=text,
hoverinfo=“text”,
xgap=3, # this
ygap=3, # and this is used to make the grid-like apperance
showscale=False,
colorscale=colorscale
)
]
layout = go.Layout(
title=‘activity chart’,
height=280,
yaxis=dict(
showline = False, showgrid = False, zeroline = False,
tickmode=“array”,
ticktext=[“Mon”, “Tue”, “Wed”, “Thu”, “Fri”, “Sat”, “Sun”],
tickvals=[0,1,2,3,4,5,6],
),
xaxis=dict(
showline = False, showgrid = False, zeroline = False,
),
font={‘size’:‘10’, ‘color’:‘#9e9e9e’},
plot_bgcolor=(‘#fff’),
margin = dict(t=40),
)

fig = go.Figure(data=data, layout=layout)
return fig

app = dash.Dash()
app.layout = html.Div([
dcc.Graph(id=‘heatmap-test’, figure=holidays(), config={‘displayModeBar’: False})
])

outcome:

2 Likes

Hey @zhsee

You can remove the ticks using ticks = ' ' in your xaxis and yaxis dict https://plot.ly/python/axes/#toggling-axes-lines-ticks-labels-and-autorange

1 Like

cool! now it become neater :slight_smile: i am run into another issue here, instead of two state my ‘z’ value now include another state (0, 1, 2) and i wish to use discrete color scale with green for 1 and dark green for 2 but it turns out giving me red and orange.

colorscale=[[0, ‘rgb(238, 238, 238)’], [1, ‘rgb(48, 137, 29)’], [2, ‘rgb(118, 207, 99)’]]

2 Likes

Colorscale maps between 0 (lowest) and 1 (highest) https://plot.ly/python/reference/#heatmap-colorscale Thus, try something like https://plot.ly/python/colorscales/#custom-discretized-heatmap-colorscale

2 Likes

i love this community so much :blush:

2 Likes

Also see Auto generated Heat map Calendar

Another solution would be to use the conditional formatting options in the DataTable: https://dash.plotly.com/datatable/conditional-formatting

I took this a little further with month labels, month separator lines, and multiple years:

import datetime
import plotly.graph_objs as go
import numpy as np
import dash_core_components as dcc
import dash_html_components as html
import dash

def display_year(z,
                 year: int = None,
                 month_lines: bool = True,
                 fig=None,
                 row: int = None):
    
    if year is None:
        year = datetime.datetime.now().year
    
    data = np.ones(365) * np.nan
    data[:len(z)] = z
    

    d1 = datetime.date(year, 1, 1)
    d2 = datetime.date(year, 12, 31)

    delta = d2 - d1
    
    month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
    month_days =   [31,    28,    31,     30,    31,     30,    31,    31,    30,    31,    30,    31]
    month_positions = (np.cumsum(month_days) - 15)/7

    dates_in_year = [d1 + datetime.timedelta(i) for i in range(delta.days+1)] #gives me a list with datetimes for each day a year
    weekdays_in_year = [i.weekday() for i in dates_in_year] #gives [0,1,2,3,4,5,6,0,1,2,3,4,5,6,…] (ticktext in xaxis dict translates this to weekdays
    
    weeknumber_of_dates = [int(i.strftime("%V")) if not (int(i.strftime("%V")) == 1 and i.month == 12) else 53
                           for i in dates_in_year] #gives [1,1,1,1,1,1,1,2,2,2,2,2,2,2,…] name is self-explanatory
    text = [str(i) for i in dates_in_year] #gives something like list of strings like ‘2018-01-25’ for each date. Used in data trace to make good hovertext.
    #4cc417 green #347c17 dark green
    colorscale=[[False, '#eeeeee'], [True, '#76cf63']]
    
    # handle end of year
    

    data = [
        go.Heatmap(
            x=weeknumber_of_dates,
            y=weekdays_in_year,
            z=data,
            text=text,
            hoverinfo='text',
            xgap=3, # this
            ygap=3, # and this is used to make the grid-like apperance
            showscale=False,
            colorscale=colorscale
        )
    ]
    
        
    if month_lines:
        kwargs = dict(
            mode='lines',
            line=dict(
                color='#9e9e9e',
                width=1
            ),
            hoverinfo='skip'
            
        )
        for date, dow, wkn in zip(dates_in_year,
                                  weekdays_in_year,
                                  weeknumber_of_dates):
            if date.day == 1:
                data += [
                    go.Scatter(
                        x=[wkn-.5, wkn-.5],
                        y=[dow-.5, 6.5],
                        **kwargs
                    )
                ]
                if dow:
                    data += [
                    go.Scatter(
                        x=[wkn-.5, wkn+.5],
                        y=[dow-.5, dow - .5],
                        **kwargs
                    ),
                    go.Scatter(
                        x=[wkn+.5, wkn+.5],
                        y=[dow-.5, -.5],
                        **kwargs
                    )
                ]
                    
                    
    layout = go.Layout(
        title='activity chart',
        height=250,
        yaxis=dict(
            showline=False, showgrid=False, zeroline=False,
            tickmode='array',
            ticktext=['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
            tickvals=[0, 1, 2, 3, 4, 5, 6],
            autorange="reversed"
        ),
        xaxis=dict(
            showline=False, showgrid=False, zeroline=False,
            tickmode='array',
            ticktext=month_names,
            tickvals=month_positions
        ),
        font={'size':10, 'color':'#9e9e9e'},
        plot_bgcolor=('#fff'),
        margin = dict(t=40),
        showlegend=False
    )

    if fig is None:
        fig = go.Figure(data=data, layout=layout)
    else:
        fig.add_traces(data, rows=[(row+1)]*len(data), cols=[1]*len(data))
        fig.update_layout(layout)
        fig.update_xaxes(layout['xaxis'])
        fig.update_yaxes(layout['yaxis'])
    
    return fig


def display_years(z, years):
    fig = make_subplots(rows=len(years), cols=1, subplot_titles=years)
    for i, year in enumerate(years):
        data = z[i*365 : (i+1)*365]
        display_year(data, year=year, fig=fig, row=i)
        fig.update_layout(height=250*len(years))
        
    return fig

    
z = np.random.randint(2, size=(500,))

display_years(z, (2019, 2020))

6 Likes

SO nice @bendichter !

1 Like

Hello Ben,

Thank you for sharing the code. It is very helpful.

When you generate the chart for 2022,

  1. 2022-12-31 shows up beyond the December month_line,
  2. 2022-01-01 is completely missing
  3. 2022-01-02 shows up at the end of the chart.
    (screenshots attached)


The expected behaviour would be that the 01-01-2022 and 02-01-2022 are at the beginning of the chart, 2022-12-31 shows up inside of December 2022 month, 2023-01-01 is not on the chart.

I am not sharing the code as I used copy-paste of your code
Please see screenshots attached.

Another questions is how to make the colour scale discrete? z-values in my case are 0,1,2 and I would like to give them fixed colours. How do I do that?

I will appreciate you help.
Thank you
Emil

1 Like

@emilmajkowski thanks for pointing out these issues. I I made some corrections in the gist. Does it work for you now?

1 Like

Hello Ben,

Thank you for your reply. Yes, it works perfectly fine now. Thanks a lot!

Btw. I fixed the colours to 0,1,2 values by providing zmin and zmax in the go.Heatmap

Best,
Emil