@@ -877,6 +877,7 @@ def draw(self, renderer):
877877class PathCollection (_CollectionWithSizes ):
878878 """
879879 This is the most basic :class:`Collection` subclass.
880+ A :class:`PathCollection` is e.g. created by a :meth:`~.Axes.scatter` plot.
880881 """
881882 @docstring .dedent_interpd
882883 def __init__ (self , paths , sizes = None , ** kwargs ):
@@ -899,6 +900,121 @@ def set_paths(self, paths):
899900 def get_paths (self ):
900901 return self ._paths
901902
903+ def legend_elements (self , prop = "colors" , num = "auto" ,
904+ fmt = None , func = lambda x : x , ** kwargs ):
905+ """
906+ Creates legend handles and labels for a PathCollection. This is useful
907+ for obtaining a legend for a :meth:`~.Axes.scatter` plot. E.g.::
908+
909+ scatter = plt.scatter([1,2,3], [4,5,6], c=[7,2,3])
910+ plt.legend(*scatter.legend_elements())
911+
912+ Also see the :ref:`automatedlegendcreation` example.
913+
914+ Parameters
915+ ----------
916+ prop : string, optional, default *"colors"*
917+ Can be *"colors"* or *"sizes"*. In case of *"colors"*, the legend
918+ handles will show the different colors of the collection. In case
919+ of "sizes", the legend will show the different sizes.
920+ num : int or None or string "auto", optional (default "auto")
921+ Target number of elements to create.
922+ If None, use all unique elements of the mappable array. If an
923+ integer, target to use *num* elements in the normed range.
924+ If *"auto"*, try to determine which option better suits the nature
925+ of the data.
926+ The number of created elements may slightly deviate from *num* due
927+ to a `~.ticker.Locator` being used to find useful locations.
928+ fmt : string, `~matplotlib.ticker.Formatter`, or None (default)
929+ The format or formatter to use for the labels. If a string must be
930+ a valid input for a `~.StrMethodFormatter`. If None (the default),
931+ use a `~.ScalarFormatter`.
932+ func : function, default *lambda x: x*
933+ Function to calculate the labels. Often the size (or color)
934+ argument to :meth:`~.Axes.scatter` will have been pre-processed
935+ by the user using a function *s = f(x)* to make the markers
936+ visible; e.g. *size = np.log10(x)*. Providing the inverse of this
937+ function here allows that pre-processing to be inverted, so that
938+ the legend labels have the correct values;
939+ e.g. *func = np.exp(x, 10)*.
940+ kwargs : further parameters
941+ Allowed kwargs are *color* and *size*. E.g. it may be useful to
942+ set the color of the markers if *prop="sizes"* is used; similarly
943+ to set the size of the markers if *prop="colors"* is used.
944+ Any further parameters are passed onto the `.Line2D` instance.
945+ This may be useful to e.g. specify a different *markeredgecolor* or
946+ *alpha* for the legend handles.
947+
948+ Returns
949+ -------
950+ tuple (handles, labels)
951+ with *handles* being a list of `.Line2D` objects
952+ and *labels* a list of strings of the same length.
953+ """
954+ handles = []
955+ labels = []
956+ hasarray = self .get_array () is not None
957+ if fmt is None :
958+ fmt = mpl .ticker .ScalarFormatter (useOffset = False , useMathText = True )
959+ elif type (fmt ) == str :
960+ fmt = mpl .ticker .StrMethodFormatter (fmt )
961+ fmt .create_dummy_axis ()
962+
963+ if prop == "colors" and hasarray :
964+ u = np .unique (self .get_array ())
965+ size = kwargs .pop ("size" , mpl .rcParams ["lines.markersize" ])
966+ elif prop == "sizes" :
967+ u = np .unique (self .get_sizes ())
968+ color = kwargs .pop ("color" , "k" )
969+ else :
970+ warnings .warn ("Invalid prop provided, or collection without "
971+ "array used." )
972+ return handles , labels
973+
974+ fmt .set_bounds (func (u ).min (), func (u ).max ())
975+ if num == "auto" :
976+ num = 9
977+ if len (u ) <= num :
978+ num = None
979+ if num is None :
980+ values = u
981+ label_values = func (values )
982+ else :
983+ num = int (num )
984+ if prop == "colors" and hasarray :
985+ arr = self .get_array ()
986+ elif prop == "sizes" :
987+ arr = self .get_sizes ()
988+ loc = mpl .ticker .MaxNLocator (nbins = num , min_n_ticks = num - 1 ,
989+ steps = [1 , 2 , 2.5 , 3 , 5 , 6 , 8 , 10 ])
990+ label_values = loc .tick_values (func (arr ).min (), func (arr ).max ())
991+ cond = (label_values >= func (arr ).min ()) & \
992+ (label_values <= func (arr ).max ())
993+ label_values = label_values [cond ]
994+ xarr = np .linspace (arr .min (), arr .max (), 256 )
995+ values = np .interp (label_values , func (xarr ), xarr )
996+
997+ kw = dict (markeredgewidth = self .get_linewidths ()[0 ],
998+ alpha = self .get_alpha ())
999+ kw .update (kwargs )
1000+
1001+ for val , lab in zip (values , label_values ):
1002+ if prop == "colors" and hasarray :
1003+ color = self .cmap (self .norm (val ))
1004+ elif prop == "sizes" :
1005+ size = np .sqrt (val )
1006+ if np .isclose (size , 0.0 ):
1007+ continue
1008+ h = mlines .Line2D ([0 ], [0 ], ls = "" , color = color , ms = size ,
1009+ marker = self .get_paths ()[0 ], ** kw )
1010+ handles .append (h )
1011+ if hasattr (fmt , "set_locs" ):
1012+ fmt .set_locs (label_values )
1013+ l = fmt (lab )
1014+ labels .append (l )
1015+
1016+ return handles , labels
1017+
9021018
9031019class PolyCollection (_CollectionWithSizes ):
9041020 @docstring .dedent_interpd
0 commit comments