How to combine timeline plots with scatter plots in plotly
Asked Answered
T

1

6

I have a dataframe df that contains values over time (process data). I have a dataframe ef that contains events (alarms) that have a starting time and end time. I would like to combine them together in one plot, in a presentable way.

"""
df==
+---------------------+------------+------------+------------+
|                     |   processA |   processB |   processC |
|---------------------+------------+------------+------------|
| 2020-01-01 00:00:00 |    101.764 |   20.0079  |   0.978738 |
| 2020-01-01 00:01:00 |    102.241 |   93.3779  |  -0.977278 |
| 2020-01-01 00:02:00 |    100.95  |   -7.56786 |  -0.103219 |
| 2020-01-01 00:03:00 |    100.411 |    7.20218 |   1.45427  |
| 2020-01-01 00:04:00 |    100.761 |    6.08375 |   0.443863 |
+---------------------+------------+------------+------------+

ef==
+----+-----------------------------------------+---------------------+---------------------+----------+
|    | alm                                     | Start               | Finish              | almid    |
|----+-----------------------------------------+---------------------+---------------------+----------|
|  0 | alarm A something something has alarmed | 2020-01-01 00:00:30 | 2020-01-01 00:01:30 | alm_id A |
|  1 | alarm B something else happened         | 2020-01-01 00:01:30 | 2020-01-01 00:02:30 | alm_id B |
|  2 | alarm A something something has alarmed | 2020-01-01 00:02:00 | 2020-01-01 00:09:30 | alm_id A |
+----+-----------------------------------------+---------------------+---------------------+----------+
"""

What I have gathered from plotly's documentation, .create_gantt() is deprecated. The recommended approach is .timeline(). I have not figured out how to customize anything in a .timeline() plot, what I have so far is this: enter image description here

Immediate concerns are: how to customize the location and width of the bars. The alarm (red in color) ticks on the y axis is taking lots of space away, how to convey the information more gracefully.
A bigger question would be Am I taking the wrong approach? is there a better way (a new chart type, combo of different charts) of doing this?

PS. Starting Point:

import plotly.graph_objects as go
import plotly.express as px
import pandas as pd
import numpy as np
import plotly.io as pio
pio.renderers.default = "browser"

np.random.seed(0)
rowcounts = 10
df = pd.DataFrame(
    index = pd.date_range(start=pd.Timestamp('2020-01-01'), freq = 'min', periods=rowcounts, ),
    data = np.random.randn(rowcounts, 3), columns=['process'+ x for x in list('ABC')],
)
df.processA += 100
df.processB *= 50

idx = pd.date_range(start=pd.Timestamp('2020-01-01'), freq = '30S', periods=rowcounts*2, )
ef = pd.DataFrame([
    dict(alm="alarm A something something has alarmed", Start=idx[1], Finish=idx[3], almid="alm_id A"),
    dict(alm="alarm B something else happened", Start=idx[3], Finish=idx[5], almid="alm_id B"),
    dict(alm="alarm A something something has alarmed", Start=idx[4], Finish=idx[-1], almid="alm_id A")
])

"""
df==
+---------------------+------------+------------+------------+
|                     |   processA |   processB |   processC |
|---------------------+------------+------------+------------|
| 2020-01-01 00:00:00 |    101.764 |   20.0079  |   0.978738 |
| 2020-01-01 00:01:00 |    102.241 |   93.3779  |  -0.977278 |
| 2020-01-01 00:02:00 |    100.95  |   -7.56786 |  -0.103219 |
| 2020-01-01 00:03:00 |    100.411 |    7.20218 |   1.45427  |
| 2020-01-01 00:04:00 |    100.761 |    6.08375 |   0.443863 |
+---------------------+------------+------------+------------+

ef==
+----+-----------------------------------------+---------------------+---------------------+----------+
|    | alm                                     | Start               | Finish              | almid    |
|----+-----------------------------------------+---------------------+---------------------+----------|
|  0 | alarm A something something has alarmed | 2020-01-01 00:00:30 | 2020-01-01 00:01:30 | alm_id A |
|  1 | alarm B something else happened         | 2020-01-01 00:01:30 | 2020-01-01 00:02:30 | alm_id B |
|  2 | alarm A something something has alarmed | 2020-01-01 00:02:00 | 2020-01-01 00:09:30 | alm_id A |
+----+-----------------------------------------+---------------------+---------------------+----------+
"""

fig = px.timeline(
    ef, x_start="Start", x_end="Finish", y="alm", color="almid",
    color_discrete_map={
        "alm_id A": 'rgba(222, 111, 222, .2)',
        "alm_id B": 'rgba(111, 222, 222, .2)',
    }
)

myfont=dict(
    color="red",
    family="monospace",
    size=14,
)
for c in df.columns:
    fig = fig.add_trace(
        go.Scatter(
            x=df.index,
            y=df[c],
            name = c,
            yaxis = 'y2',
            hovertemplate = '%{y} <extra></extra>',
        ))
    
fig = fig.update_layout(
    xaxis=dict(
        type="date",
        domain=[0.15,1],
        range = [idx[2], idx[10]],
        rangeslider=dict(
            autorange=True,
            visible=True,
        ),
    ),
    yaxis=dict(
        title="alarms",
        titlefont=myfont,
        tickfont=myfont,
    ),
    yaxis2=dict(
        title="processes",
        anchor="free",
        overlaying="y",
        side="left",
        position=0.05
    ),
)

fig = fig.update_yaxes(showline=True, linewidth=2, linecolor='black')
fig.show()
Traceable answered 14/12, 2020 at 20:7 Comment(0)
T
0

One possible aternative is doing it this way:

import plotly.graph_objects as go
import plotly.express as px
import pandas as pd
import numpy as np
import plotly.io as pio

np.random.seed(0)
rowcounts = 10
df = pd.DataFrame(
    index = pd.date_range(start=pd.Timestamp('2020-01-01'), freq = 'min', periods=rowcounts, ),
    data = np.random.randn(rowcounts, 3), columns=['process'+ x for x in list('ABC')],
)
df.processA += 100
df.processB *= 50

idx = pd.date_range(start=pd.Timestamp('2020-01-01'), freq = '30S', periods=rowcounts*2, )
ef = pd.DataFrame([
    dict(alm="alarm A something something has alarmed", Start=idx[1], Finish=idx[3], almid="alm_id A"),
    dict(alm="alarm B something else happened", Start=idx[3], Finish=idx[5], almid="alm_id B"),
    dict(alm="alarm A something something has alarmed", Start=idx[4], Finish=idx[-1], almid="alm_id A")
])

which is your data

                       processA   processB  processC
2020-01-01 00:00:00  101.764052  20.007860  0.978738
2020-01-01 00:01:00  102.240893  93.377900 -0.977278
2020-01-01 00:02:00  100.950088  -7.567860 -0.103219
2020-01-01 00:03:00  100.410599   7.202179  1.454274
2020-01-01 00:04:00  100.761038   6.083751  0.443863
2020-01-01 00:05:00  100.333674  74.703954 -0.205158
2020-01-01 00:06:00  100.313068 -42.704787 -2.552990
2020-01-01 00:07:00  100.653619  43.221810 -0.742165
2020-01-01 00:08:00  102.269755 -72.718284  0.045759
2020-01-01 00:09:00   99.812816  76.638961  1.469359

and to plot it

import plotly.graph_objects as go
import pandas as pd
import numpy as np


fig = go.Figure()

for process in df.columns:
    fig.add_trace(go.Scatter(x=df.index, y=df[process], mode='lines', name=process))

for _, alarm in ef.iterrows():
    fig.add_shape(type="rect",
                  x0=alarm['Start'], y0=0, x1=alarm['Finish'], y1=1,
                  xref='x', yref='paper',
                  fillcolor="Red", opacity=0.2, line_width=0, layer="below")

fig.update_layout(
    yaxis_title='Process Values',
    xaxis=dict(title='Time', domain=[0, 0.95]),
    legend_title='Process',
    hovermode="x unified",
    margin=dict(l=20, r=20, t=20, b=20),
    height=400, width=800,
)

fig.show()

which gives

enter image description here

and

enter image description here

Tawana answered 27/2 at 13:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.