Skip to content

Commit 89be97a

Browse files
authored
Add legend_cols and legend_opts options (#1636)
1 parent fa5219f commit 89be97a

File tree

4 files changed

+155
-0
lines changed

4 files changed

+155
-0
lines changed

doc/ref/plotting_options/legend.ipynb

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,97 @@
8282
"source": [
8383
"plot7 + plot8"
8484
]
85+
},
86+
{
87+
"cell_type": "markdown",
88+
"id": "e7f37498-781a-422b-9338-1b8d7d536076",
89+
"metadata": {},
90+
"source": [
91+
"(option-legend_cols)=\n",
92+
"## `legend_cols`\n",
93+
"\n",
94+
"The `legend_cols` option allows to define the number of columns of the legend grid."
95+
]
96+
},
97+
{
98+
"cell_type": "code",
99+
"execution_count": null,
100+
"id": "c1e9c1dd-16fb-4685-b0be-619ce21227cd",
101+
"metadata": {},
102+
"outputs": [],
103+
"source": [
104+
"import hvplot.pandas # noqa\n",
105+
"import numpy as np\n",
106+
"import pandas as pd\n",
107+
"\n",
108+
"df = pd.DataFrame({\"y\": np.random.random(20), \"cat\": list(map(chr, range(97, 117)))})\n",
109+
"\n",
110+
"df.hvplot.scatter(by=\"cat\", height=250, legend_cols=3, title=\"legend_cols=3\")"
111+
]
112+
},
113+
{
114+
"cell_type": "markdown",
115+
"id": "80cc2580-8c29-4fab-8947-8d3f3f937373",
116+
"metadata": {},
117+
"source": [
118+
"(option-legend_opts)=\n",
119+
"## `legend_opts`\n",
120+
"\n",
121+
"The `legend_opts` option allows to customize the legend styling. For the Bokeh plotting backend, the dictionary keys should be properties of its [`Legend` model](https://docs.bokeh.org/en/latest/docs/reference/models/annotations.html#bokeh.models.Legend), such as `background_fill_alpha`, `background_fill_color`, etc."
122+
]
123+
},
124+
{
125+
"cell_type": "code",
126+
"execution_count": null,
127+
"id": "0dafe596-72d3-4b6f-8484-ee3621bfb063",
128+
"metadata": {},
129+
"outputs": [],
130+
"source": [
131+
"import hvplot.pandas # noqa\n",
132+
"\n",
133+
"df = hvplot.sampledata.penguins(\"pandas\")\n",
134+
"\n",
135+
"df.hvplot.scatter(\n",
136+
" x=\"bill_length_mm\", y=\"bill_depth_mm\", by=\"species\",\n",
137+
" legend_opts={\"background_fill_alpha\": 0.2, \"background_fill_color\": \"grey\", \"spacing\": 20}\n",
138+
")"
139+
]
140+
},
141+
{
142+
"cell_type": "markdown",
143+
"id": "4f2d87e8-8821-42d7-a71f-bb5daeaf0aed",
144+
"metadata": {},
145+
"source": [
146+
"For the Matplotlib plotting backend, the keys should be keyword arguments of its [`Axes.legend` method](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.legend.html), such as `framealpha`, `facecolor`, etc."
147+
]
148+
},
149+
{
150+
"cell_type": "code",
151+
"execution_count": null,
152+
"id": "739b34ee-ebf2-4acc-9a9b-3e659b1c6a74",
153+
"metadata": {},
154+
"outputs": [],
155+
"source": [
156+
"import hvplot.pandas # noqa\n",
157+
"hvplot.extension(\"matplotlib\")\n",
158+
"\n",
159+
"df = hvplot.sampledata.penguins(\"pandas\")\n",
160+
"\n",
161+
"df.hvplot.scatter(\n",
162+
" x=\"bill_length_mm\", y=\"bill_depth_mm\", by=\"species\",\n",
163+
" legend_opts={\"framealpha\": 0.2, \"facecolor\": \"grey\", \"labelspacing\": 2}\n",
164+
")"
165+
]
166+
},
167+
{
168+
"cell_type": "markdown",
169+
"id": "6296c0ce-f273-402e-8ff5-110b2d6c0a40",
170+
"metadata": {},
171+
"source": [
172+
":::{seealso}\n",
173+
"The [`backend_opts`](option-backend_opts) option to customize even more the styling of a plot.\n",
174+
":::"
175+
]
85176
}
86177
],
87178
"metadata": {

doc/ref/plotting_options/styling.ipynb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@
168168
"hv.renderer('matplotlib').get_plot(plot).handles['axis'].get_frame_on()"
169169
]
170170
},
171+
{
172+
"cell_type": "markdown",
173+
"id": "adfa012f-a28c-4487-ad55-26fea7f38aa9",
174+
"metadata": {},
175+
"source": [
176+
":::{seealso}\n",
177+
"The [`legend_opts`](option-legend_opts) option to customize specifically the styling of the legend.\n",
178+
":::"
179+
]
180+
},
171181
{
172182
"cell_type": "markdown",
173183
"id": "881751c3-4dc2-4027-9b7f-147fbe64a4f6",

hvplot/converter.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,12 @@ class HoloViewsConverter:
343343
(``'top'``, ``'bottom'``, ``'left'``, ``'right'`` (default)) or a
344344
corner placement (``'top_left'``, ``'top_right'``, ``'bottom_left'``,
345345
``'bottom_right'``).
346+
legend_cols : int or None, default=None
347+
Number of columns in the legend.
348+
legend_opts : dict or None, default=None
349+
Allows setting specific styling options for the legend. They keys
350+
should be attributes of the ``Legend`` model for Bokeh and keyword
351+
arguments of the ``Axes.legen`` method for Matplotlib.
346352
347353
Interactivity Options
348354
---------------------
@@ -630,6 +636,8 @@ class HoloViewsConverter:
630636

631637
_legend_options = [
632638
'legend',
639+
'legend_cols',
640+
'legend_opts',
633641
]
634642

635643
_interactivity_options = [
@@ -835,6 +843,8 @@ def __init__(
835843
dynamic=True,
836844
grid=None,
837845
legend=None,
846+
legend_cols=None,
847+
legend_opts=None,
838848
rot=None,
839849
title=None,
840850
xlim=None,
@@ -1046,6 +1056,11 @@ def __init__(
10461056
'The legend option should be a boolean or '
10471057
f'a valid legend position (i.e. one of {list(self._legend_positions)}).'
10481058
)
1059+
if legend_cols is not None:
1060+
plot_opts['legend_cols'] = legend_cols
1061+
if legend_opts is not None:
1062+
plot_opts['legend_opts'] = legend_opts
1063+
10491064
if subcoordinate_y:
10501065
plot_opts['subcoordinate_y'] = True
10511066
if isinstance(subcoordinate_y, dict):

hvplot/tests/testoptions.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,45 @@ def test_backend_opts(self, df, backend):
596596
opts = Store.lookup_options(backend, plot, 'plot')
597597
assert opts.kwargs['backend_opts'] == bo
598598

599+
@pytest.mark.parametrize(
600+
'backend',
601+
[
602+
'bokeh',
603+
'matplotlib',
604+
pytest.param(
605+
'plotly',
606+
marks=pytest.mark.skip(reason='legend_cols not supported w/ plotly'),
607+
),
608+
],
609+
indirect=True,
610+
)
611+
def test_legend_cols(self, df, backend):
612+
plot = df.hvplot.scatter('x', 'y', by='category', legend_cols=2)
613+
opts = Store.lookup_options(backend, plot, 'plot')
614+
assert opts.kwargs['legend_cols'] == 2
615+
616+
@pytest.mark.parametrize(
617+
'backend',
618+
[
619+
'bokeh',
620+
'matplotlib',
621+
pytest.param(
622+
'plotly',
623+
marks=pytest.mark.skip(reason='legend_opts not supported w/ plotly'),
624+
),
625+
],
626+
indirect=True,
627+
)
628+
def test_legend_opts(self, df, backend):
629+
if backend == 'bokeh':
630+
lo = {'spacing': 20}
631+
elif backend == 'matplotlib':
632+
lo = {'labelspacing': 2}
633+
634+
plot = df.hvplot.scatter('x', 'y', by='category', legend_opts=lo)
635+
opts = Store.lookup_options(backend, plot, 'plot')
636+
assert opts.kwargs['legend_opts'] == lo
637+
599638

600639
@pytest.fixture(scope='module')
601640
def da():

0 commit comments

Comments
 (0)