Skip to content

Commit 3c093bd

Browse files
committed
python/ Improve plot_eaf and add examples.
1 parent 4cf8ad2 commit 3c093bd

File tree

4 files changed

+192
-41
lines changed

4 files changed

+192
-41
lines changed

python/examples/plot_01_pf_2d.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,9 @@
2424
# Plot a two objective dataset with points and lines
2525
fig = mooplot.plot_pf(data, type="p,l")
2626
fig
27+
28+
29+
# %%
30+
# Filled areas
31+
fig = mooplot.plot_pf(data, type="fill")
32+
fig

python/examples/plot_03_eaf.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""
2+
Plotting the EAF
3+
================
4+
5+
Various examples using :func:`mooplot.plot_eaf`.
6+
"""
7+
8+
import moocore
9+
import mooplot
10+
11+
# %%
12+
# Plot a 2D EAF surface
13+
# ---------------------
14+
#
15+
# The empirical attainment function can used to visualise the spread of solutions when looking at a multi-objective optimisation problem. This can be especially usefuly when comparing two different algorithms, or different parameters of the same algorithm.
16+
#
17+
# When plotting a single algorithm,the `plot_eaf` function requires a numpy array of EAF points, such as is created by the `get_eaf` function.
18+
19+
data = moocore.get_dataset("input1.dat")
20+
eafs = moocore.eaf(data, percentiles=[25, 50, 75, 100])
21+
fig = mooplot.plot_eaf(eafs)
22+
fig
23+
24+
# %%
25+
# Plot multiple algorithms on the same graph
26+
# -----------------------------------------
27+
#
28+
# To plot multiple EAF data on the same graph, such as comparing the median result of two algorithms, the `dataset` argument should be a dictionary. The `type` argument can be any of `lines`, `fill` or `points`.
29+
30+
# Generate random non-dominated data points for demonstration
31+
data1 = moocore.get_dataset("wrots_l10w100_dat.xz")
32+
data2 = moocore.get_dataset("wrots_l100w10_dat.xz")
33+
34+
eaf1 = moocore.eaf(data1, percentiles=[0, 25, 50, 75, 100])
35+
eaf2 = moocore.eaf(data2, percentiles=[0, 25, 50, 75, 100])
36+
37+
fig = mooplot.plot_eaf(
38+
{"l=10, w=100": eaf1, "l=100, w=10": eaf2}, type="lines", percentiles=[50]
39+
)
40+
fig
41+
42+
43+
# %%
44+
# Customising EAF plots - Combining different types and styling
45+
# -------------------------------------------------------------
46+
#
47+
# In this example, the median value (50th percentile) of algorithm 2 is compared to the filled EAF plot of algorithm 1.
48+
#
49+
# * The `type` argument can be a list defining the plot type of each dataset
50+
# * The `percentiles` argument chooses which percentiles to plot for every algorithm. It must exist in the eaf data - so ensure that it is produced from `get_eaf`
51+
# * The `colorway`, `fill_border_colours` and `percentiles` arguments can accept a 2d list, configuring each EAF seperately
52+
# * The "colorway" argument configures the colours of the traces. [See more about colorway](colorway-section)
53+
# * The `fill_border_colours` argument defines the colour of the percentile boundary lines in a fill plot. It can be set to `"rgba(0,0,0,0)"` (invisible) to remove them.
54+
# * The `trace_names` argument can over-ride the default figure names. The default is: "{Dictonary key name} - {percentile}"
55+
# * Plotly layout named arguments can be used such as `legend_title_text`. See [style plots page](style-plots-section)
56+
#
57+
58+
colorway1 = ["darkgrey", "grey", "black"]
59+
60+
fig = mooplot.plot_eaf(
61+
{"l=10, w=100": eaf1, "l=100, w=10": eaf2},
62+
type=["fill", "line"],
63+
percentiles=[[0, 50, 100], [50]],
64+
colorway=[colorway1, "darkblue"],
65+
fill_border_colours="rgba(0,0,0,0)",
66+
trace_names=[
67+
"Algorithm 1 Best",
68+
"Algorithm 1 Median",
69+
"Algorithm 1 Worst",
70+
"Algorithm 2 Median",
71+
],
72+
legend_title_text="Cool legend title",
73+
)
74+
fig
75+
76+
# %%
77+
# Emphasize algorithms using `line_dashes` and `line_width`
78+
# ---------------------------------------------------------
79+
#
80+
# Using the properties `line_dashes` and `line_width` you certain lines/ algorithms can be be emphasized.
81+
#
82+
# For example in this plot, the second algorithm is emphasized by making its best and worst lines thicker than others, and by making its `line_dashes` argument different to the other algorithms.
83+
#
84+
# `line_dashes` Defines whether lines are solid, dashed, dotted etc, It can be one of: 'solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'. It can be accept one of:
85+
# 1. A single value defining the type for all lines in the plot
86+
# 2. A list with same length as the number of different algorithms, setting the line type for each algorithm
87+
# 3. A 2d list, with each sub-list containg values for every trace within the algorithm
88+
# 1. A combination of argument types (2 and 3) eg a single value and list is also accepted (see example below)
89+
#
90+
# `line_width` changes the line thickness. It's can also be a single value, list or 2d list as with `line_dashes`
91+
#
92+
93+
fig = mooplot.plot_eaf(
94+
{"l=10, w=100": eaf1, "l=100, w=10": eaf2},
95+
type="lines",
96+
percentiles=[0, 100],
97+
colorway=["blue", "darkgreen"],
98+
line_dashes=["dot", ["solid", "dash"]],
99+
line_width=[
100+
1,
101+
3,
102+
], # Emphasize second algorithm by making the lines thicker
103+
)
104+
fig
105+
106+
# %%
107+
# Emphasize algorithms using `line_dashes` and `line_width`
108+
# ---------------------------------------------------------
109+
#
110+
# The `legend_preset` argument of `plot_eaf` can be used to configure the legends position to one of the presets, or to set the title text, background colour and border colour of the legend.
111+
#
112+
# `legend_preset` can be a string setting the legend position preset. The preset positions available are: `"outside_top_right"`, `"outside_top_left"`, `"top_right"`, `"bottom_right"`, `"top_left"`, `"bottom_left"`,`"centre_top_right"`, `"centre_top_left"`,`"centre_bottom_right"`, `"centre_bottom_left"`. The default value is `"centre_top_right"`
113+
#
114+
# `legend_preset` can also be a list or dictionary describing the **legend position**, **title text**, **background colour** and **border colour**:
115+
# * `legend_preset = dict(position = "top_right", text="Legend title text", colour = "black", border_colour="darkblue")` # Dictionary argument
116+
# * `legend_preset =["top_left", "Legend title text", "grey", black]` list argument. Set an argument to `None` if you don't want to change something.
117+
# You can set the text argument to "" to make the legend title disappear, or set the colour a "invisible" to remove the legend background or border.
118+
#
119+
# For any more complex legend requirements, see the [plotly legend documentation](https://plotly.com/python/legend/)
120+
#
121+
122+
# %%
123+
# ### Legend example - Position and background colour ###
124+
#
125+
# Here the legend is set to be outside the plot, the legend title is removed, and its background is set to a transparent blue colour
126+
127+
dat = moocore.get_dataset("input1.dat")
128+
eafs = moocore.eaf(dat, percentiles=[0, 50, 100])
129+
fig = mooplot.plot_eaf(
130+
eafs,
131+
legend_preset=["outside_top_right", "", "rgba(50,192,192, 0.5)", None],
132+
trace_names=["Worst", "Median", "Best"],
133+
)
134+
fig
135+
136+
# %%
137+
# ### Legend example - Border colour ###
138+
#
139+
# In this example the dictionary interface is used. The legend position and legend border colour is updated.
140+
#
141+
142+
eafs = moocore.get_eaf(dat, percentiles=[0, 33, 66, 100])
143+
colorway = mooplot.colour.discrete_colour_gradient("lightblue", "darkblue", 4)
144+
fig = mooplot.plot_eaf(
145+
eafs,
146+
type="lines",
147+
colorway=colorway,
148+
line_width=4,
149+
legend_preset=dict(position="top_right", border_colour="darkblue"),
150+
)
151+
fig

python/src/mooplot/_plot.py

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ def plot_pf(
5454
Whether to automatically filter dominated points within each set. Default is ``True``.
5555
layout_kwargs :
5656
Update features of the graph such as title axis titles, colours etc.
57-
These additional parameters are passed to plotly update_layout. See here for all the layout features that can be accessed: `Layout Plotly reference <https://plotly.com/python-api-reference/generated/plotly.graph_objects.Layout.html#plotly.graph_objects.Layout/>`_
57+
These additional parameters are passed to plotly :meth:`plotly.graph_objects.Figure.update_layout`.
58+
See :class:`plotly.graph_objects.Layout`.
5859
5960
Returns
6061
-------
61-
A `Plotly GO figure` object: `Plotly Figure reference <https://plotly.com/python-api-reference/generated/plotly.graph_objects.Figure.html#id0/>`_
62-
This means that the user can customise any part of the graph after it is created
62+
Graphical object. The user can customise any part of the graph after it is created.
6363
6464
Examples
6565
--------
@@ -99,13 +99,11 @@ def plot_pf(
9999
if type_parsed == "fill":
100100
def_colours = colour.get_default_fill_colorway(num_percentiles)
101101
colorway = colour.parse_colorway(
102-
dict.get(layout_kwargs, "colorway", None),
103-
def_colours,
102+
dict.get(layout_kwargs, "colorway", def_colours),
104103
num_percentiles,
105104
)
106105
fill_border_colours = colour.parse_colorway(
107-
dict.get(layout_kwargs, "fill_border_colours", None),
108-
def_colours,
106+
dict.get(layout_kwargs, "fill_border_colours", def_colours),
109107
num_percentiles,
110108
)
111109
figure = create_2d_eaf_plot(data, colorway, fill_border_colours)
@@ -115,14 +113,15 @@ def plot_pf(
115113

116114
else:
117115
colorway = colour.parse_colorway(
118-
dict.get(layout_kwargs, "colorway", None),
119-
px.colors.qualitative.Plotly,
116+
dict.get(
117+
layout_kwargs, "colorway", px.colors.qualitative.Plotly
118+
),
120119
num_percentiles,
121120
)
122121
layout_kwargs["colorway"] = colorway
123122
# Sort the the points by Objective 1 within each set, while keeping the set order (May be inefficient)
124-
for set in df["Set"].unique():
125-
mask = df["Set"] == set
123+
for s in df["Set"].unique():
124+
mask = df["Set"] == s
126125
df.loc[mask] = (
127126
df.loc[mask].sort_values(by=df.columns[0]).values
128127
)
@@ -135,6 +134,7 @@ def plot_pf(
135134
color_discrete_sequence=colorway,
136135
)
137136

137+
# FIXME: This should be configurable.
138138
maximise = [False, False]
139139
# Extend lines past the figure boundaries.
140140
for trace in figure.data:
@@ -144,8 +144,7 @@ def plot_pf(
144144

145145
elif dim == 3:
146146
colorway = colour.parse_colorway(
147-
dict.get(layout_kwargs, "colorway", None),
148-
px.colors.qualitative.Plotly,
147+
dict.get(layout_kwargs, "colorway", px.colors.qualitative.Plotly),
149148
num_percentiles,
150149
)
151150
title = layout_kwargs.pop("title", None)
@@ -340,7 +339,7 @@ def add_extremes(x, y, maximise):
340339

341340
# Create a fill plot -> Such as EAF percentile plot.
342341
# If a figure is given, update the figure instead of creating a new one
343-
# If no name is given, the last column eg. Percentile is chosen. Names can be a list or a
342+
# If no name is given, the last column eg. Percentile is chosen.
344343
def create_2d_eaf_plot(
345344
dataset,
346345
colorway,
@@ -350,20 +349,19 @@ def create_2d_eaf_plot(
350349
names=None,
351350
line_dashes=None,
352351
line_width=None,
353-
):
352+
) -> go.Figure:
354353
# Get a line plot and sort by the last column eg. Set number or percentile
354+
# FIXME: remove this recursion.
355355
lines_plot = plot_pf(dataset, type="line", filter_dominated=False)
356356
ordered_lines = sorted(lines_plot.data, key=lambda x: int(x["name"]))
357357
float_inf = np.finfo(
358358
np.float64
359359
).max # Interpreted as infinite value by plotly
360360

361361
# Add an line to fill down from infinity to the last percentile
362-
infinity_line = dict(
363-
x=np.array([0, float_inf]),
364-
y=np.array([float_inf, float_inf]),
362+
ordered_lines.append(
363+
dict(x=np.array([0, float_inf]), y=np.array([float_inf, float_inf]))
365364
)
366-
ordered_lines.append(infinity_line)
367365
percentile_names = np.unique(dataset[:, -1]).astype(int)
368366
num_percentiles = len(percentile_names)
369367

@@ -395,14 +393,13 @@ def create_2d_eaf_plot(
395393
fill_i = i - 1 if i > 0 else i #
396394
name_i = fill_i if is_fill else min(i, num_percentiles - 1)
397395

398-
select_names = (
399-
f"{names[name_i]} - {percentile_names[name_i]}"
400-
if names
401-
else percentile_names[name_i]
402-
)
403-
select_legend_group = (
404-
names[name_i] if names else percentile_names[name_i]
405-
)
396+
if names:
397+
select_names = f"{names[name_i]} - {percentile_names[name_i]}"
398+
select_legend_group = names[name_i]
399+
else:
400+
select_names = percentile_names[name_i]
401+
select_legend_group = percentile_names[name_i]
402+
406403
line_colour = (
407404
fill_border_colours[name_i] if type == "fill" else colorway[name_i]
408405
)
@@ -414,7 +411,7 @@ def create_2d_eaf_plot(
414411
fill="none" if (i == 0 or not is_fill) else "tonexty",
415412
line={
416413
"shape": "hv",
417-
"dash": f"{line_dashes[name_i]}",
414+
"dash": line_dashes[name_i],
418415
"color": line_colour,
419416
"width": line_width[name_i],
420417
},
@@ -619,10 +616,11 @@ def plot_eaf(
619616
num_percentiles = len(np.unique(dataset[:, -1]))
620617
def_colours = colour.get_default_fill_colorway(num_percentiles)
621618
colorway = colour.parse_colorway(
622-
colorway, def_colours, num_percentiles
619+
colorway if colorway else def_colours, num_percentiles
623620
)
624621
fill_border_colours = colour.parse_colorway(
625-
fill_border_colours, def_colours, num_percentiles
622+
fill_border_colours if fill_border_colours else def_colours,
623+
num_percentiles,
626624
)
627625

628626
fig = create_2d_eaf_plot(
@@ -637,7 +635,7 @@ def plot_eaf(
637635
legend_title_text="Percentile",
638636
xaxis_title="Objective 0",
639637
yaxis_title="Objective 1",
640-
title="2d Empirical Attainment Function",
638+
title="2D Empirical Attainment Function",
641639
)
642640

643641
elif isinstance(dataset, dict):
@@ -658,9 +656,8 @@ def plot_eaf(
658656
raise TypeError("Incorrect type for percentiles")
659657

660658
if isinstance(type, str):
661-
type = [type] * len(
662-
dataset
663-
) # Set all types to be single type argument
659+
# Set all types to be single type argument
660+
type = [type] * len(dataset)
664661
elif len(type) != len(dataset):
665662
raise ValueError(
666663
"type list must be same length as dataset dictionary"

python/src/mooplot/colour.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,7 @@ def create_gradient(self, steps):
195195

196196

197197
# Parse different types of colorway arguments into an acceptable format, or choose default
198-
def parse_colorway(colorway, default, length):
199-
colorway = colorway if colorway else default
198+
def parse_colorway(colorway, length):
200199
if isinstance(colorway, str) or isinstance(colorway, int):
201200
colorway = parse_colour_to_nparray(colorway, strings=True)
202201
colorway = [colorway] * length
@@ -221,17 +220,15 @@ def parse_2d_colorway(colorway, default, size_list):
221220
parsed_2d = colorway if colorway else default
222221
if isinstance(parsed_2d, str):
223222
# Set all traces to be same single value
224-
parsed_2d = [
225-
parse_colorway(parsed_2d, default, size) for size in size_list
226-
]
223+
parsed_2d = [parse_colorway(parsed_2d, size) for size in size_list]
227224
elif isinstance(parsed_2d, list):
228-
# Can individually select each trace, or set to the same for each
225+
# Can individually select each trace, or set to the same for each.
229226
if len(parsed_2d) != len(size_list):
230227
raise ValueError(
231228
"2d colorway list should be same length as number of traces"
232229
)
233230
parsed_2d = [
234-
parse_colorway(color_i, default, size_list[i])
231+
parse_colorway(color_i if color_i else default, size_list[i])
235232
for i, color_i in enumerate(parsed_2d)
236233
]
237234
else:

0 commit comments

Comments
 (0)