477 self.ContextualButtonsItem) |
484 self.ContextualButtonsItem) |
478 # Close contextual menu |
485 # Close contextual menu |
479 self.DismissContextualButtons() |
486 self.DismissContextualButtons() |
480 |
487 |
481 def HandleCursorMove(self, event): |
488 def HandleCursorMove(self, event): |
|
489 """ |
|
490 Update Cursor position according to mouse position and graph type |
|
491 @param event: Mouse event |
|
492 """ |
482 start_tick, end_tick = self.ParentWindow.GetRange() |
493 start_tick, end_tick = self.ParentWindow.GetRange() |
483 cursor_tick = None |
494 cursor_tick = None |
484 items = self.ItemsDict.values() |
495 items = self.ItemsDict.values() |
|
496 |
|
497 # Graph is orthogonal |
485 if self.GraphType == GRAPH_ORTHOGONAL: |
498 if self.GraphType == GRAPH_ORTHOGONAL: |
|
499 # Extract items data displayed in canvas figure |
|
500 min_start_tick = max(start_tick, self.GetItemsMinCommonTick()) |
|
501 start_tick = max(start_tick, min_start_tick) |
|
502 end_tick = max(end_tick, min_start_tick) |
486 x_data = items[0].GetData(start_tick, end_tick) |
503 x_data = items[0].GetData(start_tick, end_tick) |
487 y_data = items[1].GetData(start_tick, end_tick) |
504 y_data = items[1].GetData(start_tick, end_tick) |
|
505 |
|
506 # Search for the nearest point from mouse position |
488 if len(x_data) > 0 and len(y_data) > 0: |
507 if len(x_data) > 0 and len(y_data) > 0: |
489 length = min(len(x_data), len(y_data)) |
508 length = min(len(x_data), len(y_data)) |
490 d = numpy.sqrt((x_data[:length,1]-event.xdata) ** 2 + (y_data[:length,1]-event.ydata) ** 2) |
509 d = numpy.sqrt((x_data[:length,1]-event.xdata) ** 2 + \ |
|
510 (y_data[:length,1]-event.ydata) ** 2) |
|
511 |
|
512 # Set cursor tick to the tick of this point |
491 cursor_tick = x_data[numpy.argmin(d), 0] |
513 cursor_tick = x_data[numpy.argmin(d), 0] |
|
514 |
|
515 # Graph is parallel |
492 else: |
516 else: |
|
517 # Extract items tick |
493 data = items[0].GetData(start_tick, end_tick) |
518 data = items[0].GetData(start_tick, end_tick) |
|
519 |
|
520 # Search for point that tick is the nearest from mouse X position |
|
521 # and set cursor tick to the tick of this point |
494 if len(data) > 0: |
522 if len(data) > 0: |
495 cursor_tick = data[numpy.argmin(numpy.abs(data[:,0] - event.xdata)), 0] |
523 cursor_tick = data[numpy.argmin( |
|
524 numpy.abs(data[:,0] - event.xdata)), 0] |
|
525 |
|
526 # Update cursor tick |
496 if cursor_tick is not None: |
527 if cursor_tick is not None: |
497 self.ParentWindow.SetCursorTick(cursor_tick) |
528 self.ParentWindow.SetCursorTick(cursor_tick) |
498 |
529 |
499 def OnCanvasButtonPressed(self, event): |
530 def OnCanvasButtonPressed(self, event): |
500 """ |
531 """ |
501 Function called when a button of mouse is pressed |
532 Function called when a button of mouse is pressed |
502 @param event: Mouse event |
533 @param event: Mouse event |
503 """ |
534 """ |
504 # Get mouse position, graph coordinates are inverted comparing to wx |
535 # Get mouse position, graph Y coordinate is inverted in matplotlib |
505 # coordinates |
536 # comparing to wx |
506 width, height = self.GetSize() |
537 width, height = self.GetSize() |
507 x, y = event.x, height - event.y |
538 x, y = event.x, height - event.y |
508 |
539 |
509 # Return immediately if mouse is over a button |
540 # Return immediately if mouse is over a button |
510 if self.IsOverButton(x, y): |
541 if self.IsOverButton(x, y): |
764 self.SetCursor(wx.NullCursor) |
782 self.SetCursor(wx.NullCursor) |
765 DebugVariableViewer.OnLeave(self, event) |
783 DebugVariableViewer.OnLeave(self, event) |
766 else: |
784 else: |
767 event.Skip() |
785 event.Skip() |
768 |
786 |
|
787 def GetCanvasMinSize(self): |
|
788 """ |
|
789 Return the minimum size of Viewer so that all items label can be |
|
790 displayed |
|
791 @return: wx.Size containing Viewer minimum size |
|
792 """ |
|
793 # The minimum height take in account the height of all items, padding |
|
794 # inside figure and border around figure |
|
795 return wx.Size(200, |
|
796 CANVAS_BORDER[0] + CANVAS_BORDER[1] + |
|
797 2 * CANVAS_PADDING + VALUE_LABEL_HEIGHT * len(self.Items)) |
|
798 |
|
799 def SetCanvasSize(self, width, height): |
|
800 """ |
|
801 Set Viewer size checking that it respects Viewer minimum size |
|
802 @param width: Viewer width |
|
803 @param height: Viewer height |
|
804 """ |
|
805 height = max(height, self.GetCanvasMinSize()[1]) |
|
806 self.SetMinSize(wx.Size(width, height)) |
|
807 self.RefreshLabelsPosition(height) |
|
808 self.ParentWindow.RefreshGraphicsSizer() |
|
809 |
|
810 def GetAxesBoundingBox(self, parent_coordinate=False): |
|
811 """ |
|
812 Return figure bounding box in wx coordinate |
|
813 @param parent_coordinate: True if use parent coordinate (default False) |
|
814 """ |
|
815 # Calculate figure bounding box. Y coordinate is inverted in matplotlib |
|
816 # figure comparing to wx panel |
|
817 width, height = self.GetSize() |
|
818 ax, ay, aw, ah = self.figure.gca().get_position().bounds |
|
819 bbox = wx.Rect(ax * width, height - (ay + ah) * height - 1, |
|
820 aw * width + 2, ah * height + 1) |
|
821 |
|
822 # If parent_coordinate, add Viewer position in parent |
|
823 if parent_coordinate: |
|
824 xw, yw = self.GetPosition() |
|
825 bbox.x += xw |
|
826 bbox.y += yw |
|
827 |
|
828 return bbox |
|
829 |
|
830 def RefreshHighlight(self, x, y): |
|
831 """ |
|
832 Refresh Viewer highlight according to mouse position |
|
833 @param x: X coordinate of mouse pointer |
|
834 @param y: Y coordinate of mouse pointer |
|
835 """ |
|
836 width, height = self.GetSize() |
|
837 |
|
838 # Mouse is over Viewer figure and graph is not 3D |
|
839 bbox = self.GetAxesBoundingBox() |
|
840 if bbox.InsideXY(x, y) and not self.Is3DCanvas(): |
|
841 rect = wx.Rect(bbox.x, bbox.y, bbox.width / 2, bbox.height) |
|
842 # Mouse is over Viewer left part of figure |
|
843 if rect.InsideXY(x, y): |
|
844 self.SetHighlight(HIGHLIGHT_LEFT) |
|
845 |
|
846 # Mouse is over Viewer right part of figure |
|
847 else: |
|
848 self.SetHighlight(HIGHLIGHT_RIGHT) |
|
849 |
|
850 # Mouse is over upper part of Viewer |
|
851 elif y < height / 2: |
|
852 # Viewer is upper one in Debug Variable Panel, show highlight |
|
853 if self.ParentWindow.IsViewerFirst(self): |
|
854 self.SetHighlight(HIGHLIGHT_BEFORE) |
|
855 |
|
856 # Viewer is not the upper one, show highlight in previous one |
|
857 # It prevents highlight to move when mouse leave one Viewer to |
|
858 # another |
|
859 else: |
|
860 self.SetHighlight(HIGHLIGHT_NONE) |
|
861 self.ParentWindow.HighlightPreviousViewer(self) |
|
862 |
|
863 # Mouse is over lower part of Viewer |
|
864 else: |
|
865 self.SetHighlight(HIGHLIGHT_AFTER) |
|
866 |
|
867 def OnAxesMotion(self, event): |
|
868 """ |
|
869 Function overriding default function called when mouse is dragged for |
|
870 rotating graph preventing refresh to be called too quickly |
|
871 @param event: Mouse event |
|
872 """ |
|
873 if self.Is3DCanvas(): |
|
874 # Call default function at most 10 times per second |
|
875 current_time = gettime() |
|
876 if current_time - self.LastMotionTime > REFRESH_PERIOD: |
|
877 self.LastMotionTime = current_time |
|
878 Axes3D._on_move(self.Axes, event) |
|
879 |
|
880 def GetAddTextFunction(self): |
|
881 """ |
|
882 Return function for adding text in figure according to graph type |
|
883 @return: Function adding text to figure |
|
884 """ |
|
885 text_func = (self.Axes.text2D if self.Is3DCanvas() else self.Axes.text) |
|
886 def AddText(*args, **kwargs): |
|
887 args = [0, 0, ""] |
|
888 kwargs["transform"] = self.Axes.transAxes |
|
889 return text_func(*args, **kwargs) |
|
890 return AddText |
|
891 |
|
892 def ResetGraphics(self): |
|
893 """ |
|
894 Reset figure and graphical elements displayed in it |
|
895 Called any time list of items or graph type change |
|
896 """ |
|
897 # Clear figure from any axes defined |
|
898 self.Figure.clear() |
|
899 |
|
900 # Add 3D projection if graph is in 3D |
|
901 if self.Is3DCanvas(): |
|
902 self.Axes = self.Figure.gca(projection='3d') |
|
903 self.Axes.set_color_cycle(['b']) |
|
904 |
|
905 # Override function to prevent too much refresh when graph is |
|
906 # rotated |
|
907 self.LastMotionTime = gettime() |
|
908 setattr(self.Axes, "_on_move", self.OnAxesMotion) |
|
909 |
|
910 # Init graph mouse event so that graph can be rotated |
|
911 self.Axes.mouse_init() |
|
912 |
|
913 # Set size of Z axis labels |
|
914 self.Axes.tick_params(axis='z', labelsize='small') |
|
915 |
|
916 else: |
|
917 self.Axes = self.Figure.gca() |
|
918 self.Axes.set_color_cycle(COLOR_CYCLE) |
|
919 |
|
920 # Set size of X and Y axis labels |
|
921 self.Axes.tick_params(axis='x', labelsize='small') |
|
922 self.Axes.tick_params(axis='y', labelsize='small') |
|
923 |
|
924 # Init variables storing graphical elements added to figure |
|
925 self.Plots = [] # List of curves |
|
926 self.VLine = None # Vertical line for cursor |
|
927 self.HLine = None # Horizontal line for cursor (only orthogonal 2D) |
|
928 self.AxesLabels = [] # List of items variable path text label |
|
929 self.Labels = [] # List of items text label |
|
930 |
|
931 # Get function to add a text in figure according to graph type |
|
932 add_text_func = self.GetAddTextFunction() |
|
933 |
|
934 # Graph type is parallel or orthogonal in 3D |
|
935 if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas(): |
|
936 num_item = len(self.Items) |
|
937 for idx in xrange(num_item): |
|
938 |
|
939 # Get color from color cycle (black if only one item) |
|
940 color = ('k' if num_item == 1 |
|
941 else COLOR_CYCLE[idx % len(COLOR_CYCLE)]) |
|
942 |
|
943 # In 3D graph items variable label are not displayed as text |
|
944 # in figure, but as axis title |
|
945 if not self.Is3DCanvas(): |
|
946 # Items variable labels are in figure upper left corner |
|
947 self.AxesLabels.append( |
|
948 add_text_func(size='small', color=color, |
|
949 verticalalignment='top')) |
|
950 |
|
951 # Items variable labels are in figure lower right corner |
|
952 self.Labels.append( |
|
953 add_text_func(size='large', color=color, |
|
954 horizontalalignment='right')) |
|
955 |
|
956 # Graph type is orthogonal in 2D |
|
957 else: |
|
958 # X coordinate labels are in figure lower side |
|
959 self.AxesLabels.append(add_text_func(size='small')) |
|
960 self.Labels.append( |
|
961 add_text_func(size='large', |
|
962 horizontalalignment='right')) |
|
963 |
|
964 # Y coordinate labels are vertical and in figure left side |
|
965 self.AxesLabels.append( |
|
966 add_text_func(size='small', rotation='vertical')) |
|
967 self.Labels.append( |
|
968 add_text_func(size='large', rotation='vertical', |
|
969 verticalalignment='top')) |
|
970 |
|
971 # Refresh position of labels according to Viewer size |
|
972 width, height = self.GetSize() |
|
973 self.RefreshLabelsPosition(height) |
|
974 |
769 def RefreshLabelsPosition(self, height): |
975 def RefreshLabelsPosition(self, height): |
770 canvas_ratio = 1. / height |
976 """ |
771 graph_ratio = 1. / ((1.0 - (CANVAS_BORDER[0] + CANVAS_BORDER[1]) * canvas_ratio) * height) |
977 Function called when mouse leave Viewer |
772 |
978 @param event: wx.MouseEvent |
|
979 """ |
|
980 # Figure position like text position in figure are expressed is ratio |
|
981 # canvas size and figure size. As we want that border around figure and |
|
982 # text position in figure don't change when canvas size change, we |
|
983 # expressed border and text position in pixel on screen and apply the |
|
984 # ratio calculated hereafter to get border and text position in |
|
985 # matplotlib coordinate |
|
986 canvas_ratio = 1. / height # Divide by canvas height in pixel |
|
987 graph_ratio = 1. / ( |
|
988 (1.0 - (CANVAS_BORDER[0] + CANVAS_BORDER[1]) * canvas_ratio) |
|
989 * height) # Divide by figure height in pixel |
|
990 |
|
991 # Update position of figure (keeping up and bottom border the same |
|
992 # size) |
773 self.Figure.subplotpars.update( |
993 self.Figure.subplotpars.update( |
774 top= 1.0 - CANVAS_BORDER[1] * canvas_ratio, |
994 top= 1.0 - CANVAS_BORDER[1] * canvas_ratio, |
775 bottom= CANVAS_BORDER[0] * canvas_ratio) |
995 bottom= CANVAS_BORDER[0] * canvas_ratio) |
776 |
996 |
|
997 # Update position of items labels |
777 if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas(): |
998 if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas(): |
778 num_item = len(self.Items) |
999 num_item = len(self.Items) |
779 for idx in xrange(num_item): |
1000 for idx in xrange(num_item): |
|
1001 |
|
1002 # In 3D graph items variable label are not displayed |
780 if not self.Is3DCanvas(): |
1003 if not self.Is3DCanvas(): |
|
1004 # Items variable labels are in figure upper left corner |
781 self.AxesLabels[idx].set_position( |
1005 self.AxesLabels[idx].set_position( |
782 (0.05, |
1006 (0.05, |
783 1.0 - (CANVAS_PADDING + AXES_LABEL_HEIGHT * idx) * graph_ratio)) |
1007 1.0 - (CANVAS_PADDING + |
|
1008 AXES_LABEL_HEIGHT * idx) * graph_ratio)) |
|
1009 |
|
1010 # Items variable labels are in figure lower right corner |
784 self.Labels[idx].set_position( |
1011 self.Labels[idx].set_position( |
785 (0.95, |
1012 (0.95, |
786 CANVAS_PADDING * graph_ratio + |
1013 CANVAS_PADDING * graph_ratio + |
787 (num_item - idx - 1) * VALUE_LABEL_HEIGHT * graph_ratio)) |
1014 (num_item - idx - 1) * VALUE_LABEL_HEIGHT * graph_ratio)) |
788 else: |
1015 else: |
789 self.AxesLabels[0].set_position((0.1, CANVAS_PADDING * graph_ratio)) |
1016 # X coordinate labels are in figure lower side |
790 self.Labels[0].set_position((0.95, CANVAS_PADDING * graph_ratio)) |
1017 self.AxesLabels[0].set_position( |
791 self.AxesLabels[1].set_position((0.05, 2 * CANVAS_PADDING * graph_ratio)) |
1018 (0.1, CANVAS_PADDING * graph_ratio)) |
792 self.Labels[1].set_position((0.05, 1.0 - CANVAS_PADDING * graph_ratio)) |
1019 self.Labels[0].set_position( |
793 |
1020 (0.95, CANVAS_PADDING * graph_ratio)) |
|
1021 |
|
1022 # Y coordinate labels are vertical and in figure left side |
|
1023 self.AxesLabels[1].set_position( |
|
1024 (0.05, 2 * CANVAS_PADDING * graph_ratio)) |
|
1025 self.Labels[1].set_position( |
|
1026 (0.05, 1.0 - CANVAS_PADDING * graph_ratio)) |
|
1027 |
|
1028 # Update subplots |
794 self.Figure.subplots_adjust() |
1029 self.Figure.subplots_adjust() |
795 |
1030 |
796 def GetCanvasMinSize(self): |
|
797 return wx.Size(200, |
|
798 CANVAS_BORDER[0] + CANVAS_BORDER[1] + |
|
799 2 * CANVAS_PADDING + VALUE_LABEL_HEIGHT * len(self.Items)) |
|
800 |
|
801 def SetCanvasSize(self, width, height): |
|
802 height = max(height, self.GetCanvasMinSize()[1]) |
|
803 self.SetMinSize(wx.Size(width, height)) |
|
804 self.RefreshLabelsPosition(height) |
|
805 self.ParentWindow.RefreshGraphicsSizer() |
|
806 |
|
807 def GetAxesBoundingBox(self, absolute=False): |
|
808 width, height = self.GetSize() |
|
809 ax, ay, aw, ah = self.figure.gca().get_position().bounds |
|
810 bbox = wx.Rect(ax * width, height - (ay + ah) * height - 1, |
|
811 aw * width + 2, ah * height + 1) |
|
812 if absolute: |
|
813 xw, yw = self.GetPosition() |
|
814 bbox.x += xw |
|
815 bbox.y += yw |
|
816 return bbox |
|
817 |
|
818 def RefreshHighlight(self, x, y): |
|
819 width, height = self.GetSize() |
|
820 bbox = self.GetAxesBoundingBox() |
|
821 if bbox.InsideXY(x, y) and not self.Is3DCanvas(): |
|
822 rect = wx.Rect(bbox.x, bbox.y, bbox.width / 2, bbox.height) |
|
823 if rect.InsideXY(x, y): |
|
824 self.SetHighlight(HIGHLIGHT_LEFT) |
|
825 else: |
|
826 self.SetHighlight(HIGHLIGHT_RIGHT) |
|
827 elif y < height / 2: |
|
828 if self.ParentWindow.IsViewerFirst(self): |
|
829 self.SetHighlight(HIGHLIGHT_BEFORE) |
|
830 else: |
|
831 self.SetHighlight(HIGHLIGHT_NONE) |
|
832 self.ParentWindow.HighlightPreviousViewer(self) |
|
833 else: |
|
834 self.SetHighlight(HIGHLIGHT_AFTER) |
|
835 |
|
836 def ResetGraphics(self): |
|
837 self.Figure.clear() |
|
838 if self.Is3DCanvas(): |
|
839 self.Axes = self.Figure.gca(projection='3d') |
|
840 self.Axes.set_color_cycle(['b']) |
|
841 self.LastMotionTime = gettime() |
|
842 setattr(self.Axes, "_on_move", self.OnAxesMotion) |
|
843 self.Axes.mouse_init() |
|
844 self.Axes.tick_params(axis='z', labelsize='small') |
|
845 else: |
|
846 self.Axes = self.Figure.gca() |
|
847 self.Axes.set_color_cycle(COLOR_CYCLE) |
|
848 self.Axes.tick_params(axis='x', labelsize='small') |
|
849 self.Axes.tick_params(axis='y', labelsize='small') |
|
850 self.Plots = [] |
|
851 self.VLine = None |
|
852 self.HLine = None |
|
853 self.Labels = [] |
|
854 self.AxesLabels = [] |
|
855 if not self.Is3DCanvas(): |
|
856 text_func = self.Axes.text |
|
857 else: |
|
858 text_func = self.Axes.text2D |
|
859 if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas(): |
|
860 num_item = len(self.Items) |
|
861 for idx in xrange(num_item): |
|
862 if num_item == 1: |
|
863 color = 'k' |
|
864 else: |
|
865 color = COLOR_CYCLE[idx % len(COLOR_CYCLE)] |
|
866 if not self.Is3DCanvas(): |
|
867 self.AxesLabels.append( |
|
868 text_func(0, 0, "", size='small', |
|
869 verticalalignment='top', |
|
870 color=color, |
|
871 transform=self.Axes.transAxes)) |
|
872 self.Labels.append( |
|
873 text_func(0, 0, "", size='large', |
|
874 horizontalalignment='right', |
|
875 color=color, |
|
876 transform=self.Axes.transAxes)) |
|
877 else: |
|
878 self.AxesLabels.append( |
|
879 self.Axes.text(0, 0, "", size='small', |
|
880 transform=self.Axes.transAxes)) |
|
881 self.Labels.append( |
|
882 self.Axes.text(0, 0, "", size='large', |
|
883 horizontalalignment='right', |
|
884 transform=self.Axes.transAxes)) |
|
885 self.AxesLabels.append( |
|
886 self.Axes.text(0, 0, "", size='small', |
|
887 rotation='vertical', |
|
888 verticalalignment='bottom', |
|
889 transform=self.Axes.transAxes)) |
|
890 self.Labels.append( |
|
891 self.Axes.text(0, 0, "", size='large', |
|
892 rotation='vertical', |
|
893 verticalalignment='top', |
|
894 transform=self.Axes.transAxes)) |
|
895 width, height = self.GetSize() |
|
896 self.RefreshLabelsPosition(height) |
|
897 |
|
898 def SetCursorTick(self, cursor_tick): |
|
899 self.CursorTick = cursor_tick |
|
900 |
|
901 def RefreshViewer(self, refresh_graphics=True): |
1031 def RefreshViewer(self, refresh_graphics=True): |
902 |
1032 |
903 if refresh_graphics: |
1033 if refresh_graphics: |
904 start_tick, end_tick = self.ParentWindow.GetRange() |
1034 start_tick, end_tick = self.ParentWindow.GetRange() |
905 |
1035 |