-
-
Notifications
You must be signed in to change notification settings - Fork 403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow Bars to be plotted on continuous axes #6145
Conversation
I've tested it out.
Getting Bokeh backend working is a big improvement for me and what I will be using most of the time. Getting Plotly backend working would be really nice and I see many users in business preferring that backend. test-bars-with-axis-series-ezgif.com-video-speed.mp4Codeimport holoviews as hv
import panel as pn
import pandas as pd
import pandas as pd
import numpy as np
from datetime import date
import param
import math
import hvplot.pandas
pn.extension("plotly", sizing_mode="stretch_width")
min_price = 90
max_price = 110
ACCENT = "#3d5a80"
POS = "#98C1D9"
NEG = "#ee6c4d"
PERIODS = {
"MS": (date(2022, 1, 1), date(2024, 12, 1)),
"ME": (date(2022, 1, 1), date(2024, 12, 1)),
"D": (date(2022, 1, 1), date(2022, 7, 1)),
"B": (date(2022, 1, 1), date(2022, 7, 1)),
"h": (date(2022, 1, 1), date(2022, 1, 8)),
}
OPTIONS={
"bokeh": {
"line_width": 5
},
"matplotlib": {
"linewidth": 5
},
"plotly": {
"line_width": 5
}
}
def _get_label(text: str):
return pn.pane.Markdown(text, margin=(-20, 5, -20, 5))
class PricePlotter(pn.viewable.Viewer):
frequency = param.Selector(default="B", objects=list(PERIODS))
start_date = param.CalendarDate(date(2022, 1, 1))
end_date = param.CalendarDate(date(2022, 4, 1))
lib = param.Selector(default="hvPlot", objects=["hvPlot", "HoloViews"])
backend = param.Selector(default="bokeh", objects=["bokeh", "matplotlib", "plotly"])
def __init__(self, **params):
super().__init__(**params)
self.settings = pn.Column(
_get_label("Frequency"),
pn.widgets.RadioButtonGroup.from_param(
self.param.frequency, button_type="primary", button_style="outline"
),
self.param.start_date,
self.param.end_date,
_get_label("Library"),
pn.widgets.RadioButtonGroup.from_param(
self.param.lib, button_type="primary", button_style="outline"
),
_get_label("Backend"),
pn.widgets.RadioButtonGroup.from_param(
self.param.backend, button_type="primary", button_style="outline"
)
)
self._layout = pn.Row(self.settings, self.get_plot)
def __panel__(self):
return self._layout
@param.depends("frequency", watch=True, on_init=True)
def _update_period(self):
start, end = PERIODS[self.frequency]
self.param.update(start_date=start, end_date=end)
@param.depends("backend", watch=True, on_init=True)
def _update_backend(self):
hv.extension(self.backend)
def get_prices(self):
dates = pd.date_range(
start=self.start_date, end=self.end_date, freq=self.frequency
)
num_values = len(dates)
values = np.random.uniform(min_price, max_price, size=num_values)
df = pd.DataFrame({"date": dates, "price": values})
df["diff"] = df["price"].diff()
df["color"] = (df["diff"] >= 0).map({True: "blue", False: "red"})
df = df.dropna()
return df
def get_plot(self):
df = self.get_prices()
df_pos = df[df["diff"] >= 0]
df_neg = df[df["diff"] <= 0]
max_price = df["price"].max()
max_y = math.ceil(max_price / 10) * 10 + 10
options=OPTIONS[self.backend]
if self.lib == "HoloViews":
abs_plot = hv.Curve(df, kdims="date", vdims="price").opts(
responsive=True,
height=500,
color=ACCENT,
ylim=(0, max_y),
tools=["hover"],
**options
)
rel_plot_pos = hv.Bars(
df_pos,
kdims="date",
vdims="diff",
).opts(responsive=True, height=200, color=POS, tools=["hover"])
rel_plot_neg = hv.Bars(
df_neg,
kdims="date",
vdims="diff",
).opts(responsive=True, height=200, color=NEG, tools=["hover"])
else:
abs_plot = df.hvplot.line(
x="date",
y="price",
responsive=True,
height=500,
color=ACCENT,
ylim=(0, max_y),
tools=["hover"],
xlabel="Date",
ylabel="Price",
**options
)
rel_plot_pos = df_pos.hvplot.bar(
x="date",
y="diff",
responsive=True,
height=200,
color=POS,
tools=["hover"],
xlabel="Date",
ylabel="Diff",
)
rel_plot_neg = df_neg.hvplot.bar(
x="date",
y="diff",
responsive=True,
height=200,
color=NEG,
tools=["hover"],
xlabel="Date",
ylabel="Diff",
)
return (abs_plot + rel_plot_pos * rel_plot_neg).cols(1)
plotter = PricePlotter()
pn.template.FastListTemplate(
title="Bars Test App", sidebar=[plotter.settings], main=[plotter.get_plot], main_layout=None, accent=ACCENT
).servable() |
Matplotlib AnalysisThis is how Matplotlib does it. Appmatplotlib-bars.mp4Codeimport panel as pn
import pandas as pd
import pandas as pd
import numpy as np
from datetime import date
import param
import matplotlib.pyplot as plt
pn.extension("plotly", sizing_mode="stretch_width")
min_price = 90
max_price = 110
ACCENT = "#3d5a80"
POS = "#98C1D9"
NEG = "#ee6c4d"
PERIODS = {
"MS": (date(2022, 1, 1), date(2024, 12, 1)),
"ME": (date(2022, 1, 1), date(2024, 12, 1)),
"D": (date(2022, 1, 1), date(2022, 7, 1)),
"B": (date(2022, 1, 1), date(2022, 7, 1)),
"h": (date(2022, 1, 1), date(2022, 1, 8)),
}
OPTIONS={
"bokeh": {
"line_width": 5
},
"matplotlib": {
"linewidth": 5
},
"plotly": {
"line_width": 5
}
}
def _get_label(text: str):
return pn.pane.Markdown(text, margin=(-20, 5, -20, 5))
class PricePlotter(pn.viewable.Viewer):
frequency = param.Selector(default="B", objects=list(PERIODS))
start_date = param.CalendarDate(date(2022, 1, 1))
end_date = param.CalendarDate(date(2022, 4, 1))
def __init__(self, **params):
super().__init__(**params)
self.settings = pn.Column(
_get_label("Frequency"),
pn.widgets.RadioButtonGroup.from_param(
self.param.frequency, button_type="primary", button_style="outline"
),
self.param.start_date,
self.param.end_date,
)
self._layout = pn.Row(self.settings, self.get_plot)
def __panel__(self):
return self._layout
@param.depends("frequency", watch=True, on_init=True)
def _update_period(self):
start, end = PERIODS[self.frequency]
self.param.update(start_date=start, end_date=end)
def get_prices(self):
dates = pd.date_range(
start=self.start_date, end=self.end_date, freq=self.frequency
)
num_values = len(dates)
values = np.random.uniform(min_price, max_price, size=num_values)
df = pd.DataFrame({"date": dates, "price": values})
df["diff"] = df["price"].diff()
df["color"] = (df["diff"] >= 0).map({True: "blue", False: "red"})
df = df.dropna()
return df
def get_plot(self):
df = self.get_prices()
df_pos = df[df["diff"] >= 0]
df_neg = df[df["diff"] <= 0]
max_price = df["price"].max()
fig1 = plt.figure(figsize=(10, 3))
plt.plot(df["date"], df["price"], color=ACCENT, label="Price")
plt.xlabel("Date")
plt.ylabel("Price")
plt.title("Price Over Time")
plt.legend()
plt.close(fig1)
# Plotting the bar plots for positive and negative differences
fig2 = plt.figure(figsize=(10, 3))
# Positive differences
plt.bar(df_pos["date"], df_pos["diff"], color=POS, label="Positive Diff")
# Negative differences
plt.bar(df_neg["date"], df_neg["diff"], color=NEG, label="Negative Diff")
plt.xlabel("Date")
plt.ylabel("Diff")
plt.title("Relative Differences Over Time")
plt.legend()
plt.close(fig2) # CLOSE THE FIGURE!
return pn.Column(
pn.pane.Matplotlib(fig1, tight=True),
pn.pane.Matplotlib(fig2, tight=True),
)
plotter = PricePlotter()
pn.template.FastListTemplate(
title="Bars Test App", sidebar=[plotter.settings], main=[plotter.get_plot], main_layout=None, accent=ACCENT
).servable() Matplotlib Bars Use a Fixed Default WidthMatplotlib uses a fixed, default width of |
Pandas
|
Plotly AnalysisPlotly adjust the bar width based on the input. Clearly the best behaviour. Appplotly-plot.mp4Codeimport panel as pn
import pandas as pd
import pandas as pd
import numpy as np
from datetime import date
import param
import plotly.express as px
from plotly.subplots import make_subplots
pn.extension("plotly", sizing_mode="stretch_width")
min_price = 90
max_price = 110
ACCENT = "#3d5a80"
POS = "#98C1D9"
NEG = "#ee6c4d"
PERIODS = {
"MS": (date(2022, 1, 1), date(2024, 12, 1)),
"ME": (date(2022, 1, 1), date(2024, 12, 1)),
"D": (date(2022, 1, 1), date(2022, 7, 1)),
"B": (date(2022, 1, 1), date(2022, 7, 1)),
"h": (date(2022, 1, 1), date(2022, 1, 8)),
}
OPTIONS={
"bokeh": {
"line_width": 5
},
"matplotlib": {
"linewidth": 5
},
"plotly": {
"line_width": 5
}
}
def _get_label(text: str):
return pn.pane.Markdown(text, margin=(-20, 5, -20, 5))
class PricePlotter(pn.viewable.Viewer):
frequency = param.Selector(default="B", objects=list(PERIODS))
start_date = param.CalendarDate(date(2022, 1, 1))
end_date = param.CalendarDate(date(2022, 4, 1))
def __init__(self, **params):
super().__init__(**params)
self.settings = pn.Column(
_get_label("Frequency"),
pn.widgets.RadioButtonGroup.from_param(
self.param.frequency, button_type="primary", button_style="outline"
),
self.param.start_date,
self.param.end_date,
)
self._layout = pn.Row(self.settings, self.get_plot)
def __panel__(self):
return self._layout
@param.depends("frequency", watch=True, on_init=True)
def _update_period(self):
start, end = PERIODS[self.frequency]
self.param.update(start_date=start, end_date=end)
def get_prices(self):
dates = pd.date_range(
start=self.start_date, end=self.end_date, freq=self.frequency
)
num_values = len(dates)
values = np.random.uniform(min_price, max_price, size=num_values)
df = pd.DataFrame({"date": dates, "price": values})
df["diff"] = df["price"].diff()
df["color"] = (df["diff"] >= 0).map({True: "pos", False: "neg"})
df = df.dropna()
return df
def get_plot(self):
df = self.get_prices()
abs_plot = px.line(
df,
x="date",
y="price",
height=400,
color_discrete_sequence=[ACCENT],
labels={"date": "Date", "price": "Price"},
title="Absolute Price Over Time",
)
rel_plot_pos = px.bar(
df,
x="date",
y="diff",
height=400,
color="color",
color_discrete_sequence=[POS, NEG],
labels={"date": "Date", "diff": "Diff"},
title="Positive Relative Differences Over Time",
)
fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.03)
# Add plots to subplots
fig.add_trace(abs_plot.data[0], row=1, col=1)
fig.add_trace(rel_plot_pos.data[0], row=2, col=1)
fig.add_trace(rel_plot_pos.data[1], row=2, col=1)
# Update layout
fig.update_layout(
height=800,
title_text="Combined Plots",
showlegend=False # Assuming you don't want individual legends
)
return fig
plotter = PricePlotter()
pn.template.FastListTemplate(
title="Bars Test App", sidebar=[plotter.settings], main=[plotter.get_plot], main_layout=None, accent=ACCENT
).servable() Implementation DetailsI've not been able to find the calculation of the bar width. I believe its done on Javascript side |
I assume it was a mistake you closed this? |
Reflection on calculation of bar width.Could the bar width be calculated by
? |
Test of Bar Width formulaIf you change the bar width calculation from xdiff = np.diff(xvals).astype('timedelta64[ms]').astype(np.int32) * width * 0.5 to xdiff = np.diff(xvals).astype('timedelta64[ms]').astype(np.int32) * width * 0.5
min_value = np.min(xdiff)
xdiff = np.full_like(xdiff, min_value) It looks like below which looks correct to me. bar-width-formula.mp4I would recommend experimenting with the width factor. Probably using 0.8 instead of 0.5. |
This is a very welcome addition! |
Similarly, it would be great to be able to use distribution models like BoxWhisker and Violin on a datetime or other continuous x axis. For instance, if one has a distribution of continuous values grouped per day |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6145 +/- ##
==========================================
+ Coverage 88.22% 88.38% +0.16%
==========================================
Files 321 322 +1
Lines 67285 67559 +274
==========================================
+ Hits 59361 59715 +354
+ Misses 7924 7844 -80 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have reviewed the code and left some comments.
I haven't yet tried out the code itself.
Co-authored-by: Simon Høxbro Hansen <simon.hansen@me.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enumerate is not needed anymore, so I removed it.
Strange, but glad it works! |
Okay, I reviewed again and it looks good to me. |
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
About 10 years late, but better than never.