@@ -753,6 +753,212 @@ def text(self, x, y, s, fontdict=None, **kwargs):
753
753
self ._add_text (t )
754
754
return t
755
755
756
+ def label_lines (self , * args , ** kwargs ):
757
+ """
758
+ Place labels at the end of lines on the chart.
759
+
760
+ Call signatures::
761
+
762
+ label_lines()
763
+ label_lines(labels)
764
+ label_lines(handles, labels)
765
+
766
+ The call signatures correspond to three different ways of how to use
767
+ this method.
768
+
769
+ The simplest way to use line_labels is without parameters. After the
770
+ lines are created with their labels, the user can call this function
771
+ which automatically applies the stored labels by the corresponding
772
+ lines. This would be most effectively used after the complete creation
773
+ of the line chart.
774
+
775
+ The second way to call this method is by specifying the first parameter
776
+ which is labels. In doing so you would be able to name the lines if
777
+ they lack names, or rename them as needed. This process occurs in the
778
+ order in which the lines were added to the chart.
779
+
780
+ The Final way to use this method is specifying both the handles and the
781
+ labels. In doing so you can specify names for select lines leaving the
782
+ rest blank. This would be useful when users would want to specify
783
+ select pieces of data to monitor when data clumps occur.
784
+
785
+ Parameters
786
+ ----------
787
+ handles : sequence of `.Artist`, optional
788
+ A list of Artists (lines) to be added to the line labels.
789
+ Use this together with *labels*, if you need full control on what
790
+ is shown in the line labels and the automatic mechanism described
791
+ above is not sufficient.
792
+
793
+ The length of handles and labels should be the same in this
794
+ case. If they are not, they are truncated to the smaller length.
795
+
796
+ labels : list of str, optional
797
+ A list of labels to show next to the artists.
798
+ Use this together with *handles*, if you need full control on what
799
+ is shown in the line labels and the automatic mechanism described
800
+ above is not sufficient.
801
+
802
+ Returns
803
+ -------
804
+ None
805
+
806
+ Notes
807
+ -----
808
+ Only line handles are supported by this method.
809
+
810
+ Examples
811
+ --------
812
+ .. plot:: gallery/text_labels_and_annotations/label_lines.py
813
+ """
814
+ handles , labels , extra_args , kwargs = mlegend ._parse_legend_args (
815
+ [self ],
816
+ * args ,
817
+ ** kwargs )
818
+ if len (extra_args ):
819
+ raise TypeError (
820
+ 'label_lines only accepts two nonkeyword arguments' )
821
+
822
+ self ._line_labels = []
823
+
824
+ def get_last_data_point (handle ):
825
+ last_x = handle .get_xdata ()[- 1 ]
826
+ last_y = handle .get_ydata ()[- 1 ]
827
+ return last_x , last_y
828
+
829
+ # Get last data point for each handle
830
+ xys = np .array ([get_last_data_point (x ) for x in handles ])
831
+
832
+ # Find the largest last x and y value
833
+ data_maxx , data_maxy = np .max (xys , axis = 0 )
834
+ # Find the smallest last y value
835
+ data_miny = np .min (xys , axis = 0 )[1 ]
836
+
837
+ # Get the figure width and height in pixels
838
+ fig_dpi_transform = self .figure .dpi_scale_trans .inverted ()
839
+ fig_bbox = self .get_window_extent ().transformed (fig_dpi_transform )
840
+ fig_width_px = fig_bbox .width * self .figure .dpi
841
+ fig_height_px = fig_bbox .height * self .figure .dpi
842
+
843
+ # Get the figure width and height in points
844
+ fig_minx , fig_maxx = self .get_xbound ()
845
+ fig_miny , fig_maxy = self .get_ybound ()
846
+ fig_width_pt = abs (fig_maxx - fig_minx )
847
+ fig_height_pt = abs (fig_maxy - fig_miny )
848
+
849
+ # Space to the left of the text converted from pixels
850
+ margin_left = 8 * (fig_width_pt / fig_width_px )
851
+ # Space to the top and bottom of the text converted from pixels
852
+ margin_vertical = 2 * (fig_height_pt / fig_height_px )
853
+
854
+ text_fontsize = 10
855
+ # Height of the text converted from pixels
856
+ text_height = text_fontsize * (fig_height_pt / fig_height_px )
857
+
858
+ # Height of each bucket
859
+ bucket_height = text_height + 2 * margin_vertical
860
+ # Total number of buckets
861
+ buckets_total = 1 + int ((fig_maxy - fig_miny - text_height ) /
862
+ bucket_height )
863
+ # Bit array to track which buckets are used
864
+ buckets_map = 0
865
+
866
+ def get_bucket_index (y ):
867
+ return int ((y - fig_miny ) / bucket_height )
868
+
869
+ # How many text SHOULD be in this bucket
870
+ bucket_densities = [0 ] * buckets_total
871
+ for xy in xys :
872
+ data_x , data_y = xy
873
+ ideal_bucket = get_bucket_index (data_y )
874
+ if ideal_bucket >= 0 and ideal_bucket < buckets_total :
875
+ bucket_densities [ideal_bucket ] += 1
876
+
877
+ def in_viewport (args ):
878
+ xy = args [2 ]
879
+ x , y = xy
880
+ return fig_minx < x < fig_maxx and fig_miny < y < fig_maxy
881
+
882
+ def by_y (args ):
883
+ xy = args [2 ]
884
+ x , y = xy
885
+ return y
886
+
887
+ bucket_offset = None
888
+ prev_ideal_bucket = - 1
889
+ # Iterate over all the handles, labels, and xy data sorted by y asc
890
+ for handle , label , xy in sorted (filter (in_viewport ,
891
+ zip (handles , labels , xys )),
892
+ key = by_y ):
893
+ data_x , data_y = xy
894
+
895
+ ideal_bucket = get_bucket_index (data_y )
896
+ if ideal_bucket != prev_ideal_bucket :
897
+ # If a bucket is dense, then we want to center the text around
898
+ # the bucket. SSo, find out how many empty buckets there are
899
+ # below the current bucket
900
+ bucket_density = bucket_densities [ideal_bucket ]
901
+ ideal_offset = bucket_density // 2
902
+ empty_buckets_below = 0
903
+ for i in range (ideal_bucket + 1 ,
904
+ ideal_bucket - ideal_offset + 1 ,
905
+ - 1 ):
906
+ # If reached bottom or bucket is not empty
907
+ if i == 0 or buckets_map & (1 << i ) == 1 :
908
+ break
909
+ empty_buckets_below += 1
910
+ bucket_offset = - min (ideal_offset , empty_buckets_below )
911
+ prev_ideal_bucket = ideal_bucket
912
+
913
+ bucket = ideal_bucket + bucket_offset
914
+ text_x , text_y = None , None
915
+
916
+ # Find the nearest empty bucket
917
+ for index in range (buckets_total ):
918
+ # 0 <= bucket_index <= buckets_total
919
+ bucket_index = max (0 , min (bucket + index , buckets_total ))
920
+ bucket_mask = 1 << bucket_index
921
+ # If this bucket is empty
922
+ if buckets_map & bucket_mask == 0 :
923
+ # Mark this bucket as used
924
+ buckets_map |= bucket_mask
925
+ text_x = data_maxx + margin_left
926
+ text_y = bucket_index * bucket_height + fig_miny
927
+ break
928
+
929
+ # If a bucket was found for the text
930
+ if text_x is not None and text_y is not None :
931
+ text_color = handle .get_color ()
932
+ line_label = self .annotate (label , (data_maxx , data_y ),
933
+ xytext = (text_x , text_y ),
934
+ fontsize = text_fontsize ,
935
+ color = text_color ,
936
+ annotation_clip = True )
937
+ self ._line_labels .append (line_label )
938
+
939
+ # A bucket is not necessarily aligned with the data point
940
+ # If space allows, center the text around the data point
941
+ # TODO(omarchehab98): group fuzzing
942
+ # for line_label in self._line_labels:
943
+ # datax, datay = line_label.xy
944
+ # targety = datay - text_height / 2
945
+ # textx, texty = line_label.get_position()
946
+ # direction = 1 if targety > texty else -1
947
+ # target_bucket = get_bucket_index(targety) + direction
948
+ # if (get_bucket_index(targety) == get_bucket_index(texty) and
949
+ # (target_bucket == -1 or
950
+ # buckets_map & (1 << target_bucket) == 0)):
951
+ # line_label.set_position((textx, targety))
952
+
953
+ def has_label_lines (self ):
954
+ return self ._line_labels is not None
955
+
956
+ def refresh_label_lines (self , * args , ** kwargs ):
957
+ for label in self ._line_labels :
958
+ label .remove ()
959
+ self ._line_labels = None
960
+ self .label_lines (* args , ** kwargs )
961
+
756
962
@cbook ._rename_parameter ("3.3" , "s" , "text" )
757
963
@docstring .dedent_interpd
758
964
def annotate (self , text , xy , * args , ** kwargs ):
0 commit comments