2020"""
2121
2222import math
23+ from datetime import datetime
2324
2425from qtpy .QtCore import (
2526 QLineF ,
2829 QPointF ,
2930 QRect ,
3031 QRectF ,
31- QSize ,
3232 Qt ,
3333 qFuzzyCompare ,
3434)
35- from qtpy .QtGui import QFont , QFontMetrics , QPainter , QPalette , QPixmap , QTransform
35+ from qtpy .QtGui import QFontMetrics , QPalette , QTransform
3636
3737from qwt ._math import qwtRadians
3838from qwt .scale_div import QwtScaleDiv
@@ -761,57 +761,13 @@ def labelPosition(self, value):
761761 :param float value: Value
762762 :return: Position, where to paint a label
763763 """
764- # For backward compatibility, use a default font metrics approach
765- # when rotation is involved
766- if abs (self .labelRotation ()) > 1e-6 :
767- # We need font information for proper rotation-aware positioning
768- # Use QFontMetrics with a default font as fallback
769- default_font = QFont ()
770- return self ._labelPositionWithFont (default_font , value )
771-
772- # Original implementation for non-rotated labels
773- tval = self .scaleMap ().transform (value )
774- dist = self .spacing ()
775- if self .hasComponent (QwtAbstractScaleDraw .Backbone ):
776- dist += max ([1 , self .penWidth ()])
777- if self .hasComponent (QwtAbstractScaleDraw .Ticks ):
778- dist += self .tickLength (QwtScaleDiv .MajorTick )
779-
780- px = 0
781- py = 0
782- if self .alignment () == self .RightScale :
783- px = self .__data .pos .x () + dist
784- py = tval
785- elif self .alignment () == self .LeftScale :
786- px = self .__data .pos .x () - dist
787- py = tval
788- elif self .alignment () == self .BottomScale :
789- px = tval
790- py = self .__data .pos .y () + dist
791- elif self .alignment () == self .TopScale :
792- px = tval
793- py = self .__data .pos .y () - dist
794-
795- return QPointF (px , py )
796-
797- def _labelPositionWithFont (self , font , value ):
798- """
799- Find the position where to paint a label, taking rotation into account.
800-
801- :param QFont font: Font used for the label
802- :param float value: Value
803- :return: Position where to paint a label
804- """
805764 tval = self .scaleMap ().transform (value )
806765 dist = self .spacing ()
807766 if self .hasComponent (QwtAbstractScaleDraw .Backbone ):
808767 dist += max ([1 , self .penWidth ()])
809768 if self .hasComponent (QwtAbstractScaleDraw .Ticks ):
810769 dist += self .tickLength (QwtScaleDiv .MajorTick )
811770
812- # Add rotation-aware offset
813- dist += self ._rotatedLabelOffset (font , value )
814-
815771 px = 0
816772 py = 0
817773 if self .alignment () == self .RightScale :
@@ -829,45 +785,6 @@ def _labelPositionWithFont(self, font, value):
829785
830786 return QPointF (px , py )
831787
832- def _rotatedLabelOffset (self , font , value ):
833- """
834- Calculate the additional offset needed for a rotated label
835- to avoid overlap with the scale backbone and ticks.
836-
837- :param QFont font: Font used for the label
838- :param float value: Value for which to calculate the offset
839- :return: Additional offset distance
840- """
841- rotation = self .labelRotation ()
842- if abs (rotation ) < 1e-6 : # No rotation, no additional offset needed
843- return 0.0
844-
845- lbl , labelSize = self .tickLabel (font , value )
846- if lbl .isEmpty ():
847- return 0.0
848-
849- # Convert rotation to radians
850- angle = qwtRadians (rotation )
851- cos_a = abs (math .cos (angle ))
852- sin_a = abs (math .sin (angle ))
853-
854- # Calculate the rotated bounding box dimensions
855- width = labelSize .width ()
856- height = labelSize .height ()
857- rotated_width = width * cos_a + height * sin_a
858- rotated_height = width * sin_a + height * cos_a
859-
860- # Calculate additional offset based on scale alignment
861- additional_offset = 0.0
862- if self .alignment () in (self .LeftScale , self .RightScale ):
863- # For vertical scales, consider the horizontal extent of rotated label
864- additional_offset = max (0 , (rotated_width - width ) * 0.5 )
865- else : # TopScale, BottomScale
866- # For horizontal scales, consider the vertical extent of rotated label
867- additional_offset = max (0 , (rotated_height - height ) * 0.5 )
868-
869- return additional_offset
870-
871788 def drawTick (self , painter , value , len_ ):
872789 """
873790 Draw a tick
@@ -1041,120 +958,11 @@ def drawLabel(self, painter, value):
1041958 lbl , labelSize = self .tickLabel (painter .font (), value )
1042959 if lbl is None or lbl .isEmpty ():
1043960 return
1044- pos = self ._labelPositionWithFont (painter .font (), value )
1045-
1046- # For rotated text, choose rendering method based on rotation angle
1047- rotation = self .labelRotation ()
1048- if abs (rotation ) > 1e-6 :
1049- # Check if rotation is a multiple of 90 degrees (within tolerance)
1050- normalized_rotation = rotation % 360
1051- is_90_degree_multiple = (
1052- abs (normalized_rotation ) < 1e-6
1053- or abs (normalized_rotation - 90 ) < 1e-6
1054- or abs (normalized_rotation - 180 ) < 1e-6
1055- or abs (normalized_rotation - 270 ) < 1e-6
1056- or abs (normalized_rotation - 360 ) < 1e-6
1057- )
1058-
1059- if is_90_degree_multiple :
1060- # Use direct rendering for 90-degree multiples (crisp)
1061- transform = self .labelTransformation (pos , labelSize )
1062- painter .save ()
1063- painter .setRenderHint (QPainter .TextAntialiasing , True )
1064- painter .setWorldTransform (transform , True )
1065- lbl .draw (painter , QRect (QPoint (0 , 0 ), labelSize .toSize ()))
1066- painter .restore ()
1067- else :
1068- # Use pixmap-based rendering for arbitrary angles (aligned but slightly blurry)
1069- self ._drawRotatedTextWithAlignment (
1070- painter , lbl , pos , labelSize , rotation
1071- )
1072- else :
1073- # Use standard approach for non-rotated text
1074- transform = self .labelTransformation (pos , labelSize )
1075- painter .save ()
1076- painter .setRenderHint (QPainter .TextAntialiasing , True )
1077- painter .setWorldTransform (transform , True )
1078- lbl .draw (painter , QRect (QPoint (0 , 0 ), labelSize .toSize ()))
1079- painter .restore ()
1080-
1081- def _drawRotatedTextWithAlignment (self , painter , lbl , pos , labelSize , rotation ):
1082- """
1083- Draw rotated text with improved character alignment by rendering to an
1084- intermediate pixmap and then rotating the pixmap instead of applying
1085- transformation to text.
1086- :param QPainter painter: Painter
1087- :param QwtText lbl: Label text object
1088- :param QPointF pos: Position where to paint the label
1089- :param QSizeF labelSize: Size of the label
1090- :param float rotation: Rotation angle in degrees
1091- """
1092- # Create a pixmap to render the text without rotation first
1093- text_size = labelSize .toSize ()
1094- if text_size .width () <= 0 or text_size .height () <= 0 :
1095- return
1096-
1097- # Add some padding to prevent edge clipping
1098- padding = 2
1099- pixmap_size = text_size + QSize (padding * 2 , padding * 2 )
1100- pixmap = QPixmap (pixmap_size )
1101- pixmap .fill (Qt .transparent )
1102-
1103- # Render the text to the pixmap without any rotation
1104- pixmap_painter = QPainter (pixmap )
1105- pixmap_painter .setRenderHint (QPainter .TextAntialiasing , True )
1106-
1107- # Set font and color from QwtText
1108- if lbl .testPaintAttribute (QwtText .PaintUsingTextFont ):
1109- pixmap_painter .setFont (lbl .font ())
1110- else :
1111- pixmap_painter .setFont (painter .font ())
1112-
1113- if (
1114- lbl .testPaintAttribute (QwtText .PaintUsingTextColor )
1115- and lbl .color ().isValid ()
1116- ):
1117- pixmap_painter .setPen (lbl .color ())
1118- else :
1119- pixmap_painter .setPen (painter .pen ())
1120-
1121- # Draw text on pixmap without rotation for perfect character alignment
1122- text_rect = QRect (padding , padding , text_size .width (), text_size .height ())
1123- lbl .draw (pixmap_painter , text_rect )
1124- pixmap_painter .end ()
1125-
1126- # Now draw the pixmap with rotation
961+ pos = self .labelPosition (value )
962+ transform = self .labelTransformation (pos , labelSize )
1127963 painter .save ()
1128-
1129- # Get alignment flags for positioning
1130- flags = self .labelAlignment ()
1131- if flags == 0 :
1132- flags = self .Flags [self .alignment ()]
1133-
1134- # Calculate alignment offsets
1135- if flags & Qt .AlignLeft :
1136- x_offset = - labelSize .width ()
1137- elif flags & Qt .AlignRight :
1138- x_offset = 0.0
1139- else :
1140- x_offset = - (0.5 * labelSize .width ())
1141-
1142- if flags & Qt .AlignTop :
1143- y_offset = - labelSize .height ()
1144- elif flags & Qt .AlignBottom :
1145- y_offset = 0
1146- else :
1147- y_offset = - (0.5 * labelSize .height ())
1148-
1149- # Apply transformation and draw the pre-rendered pixmap
1150- painter .translate (pos .x (), pos .y ())
1151- painter .rotate (rotation )
1152- painter .translate (x_offset - padding , y_offset - padding )
1153-
1154- # Use smooth pixmap transform for better quality
1155- painter .setRenderHint (QPainter .SmoothPixmapTransform , True )
1156- painter .drawPixmap (0 , 0 , pixmap )
1157-
964+ painter .setWorldTransform (transform , True )
965+ lbl .draw (painter , QRect (QPoint (0 , 0 ), labelSize .toSize ()))
1158966 painter .restore ()
1159967
1160968 def boundingLabelRect (self , font , value ):
@@ -1175,7 +983,7 @@ def boundingLabelRect(self, font, value):
1175983 lbl , labelSize = self .tickLabel (font , value )
1176984 if lbl .isEmpty ():
1177985 return QRect ()
1178- pos = self ._labelPositionWithFont ( font , value )
986+ pos = self .labelPosition ( value )
1179987 transform = self .labelTransformation (pos , labelSize )
1180988 return transform .mapRect (QRect (QPoint (0 , 0 ), labelSize .toSize ()))
1181989
@@ -1231,7 +1039,7 @@ def labelRect(self, font, value):
12311039 lbl , labelSize = self .tickLabel (font , value )
12321040 if not lbl or lbl .isEmpty ():
12331041 return QRectF (0.0 , 0.0 , 0.0 , 0.0 )
1234- pos = self ._labelPositionWithFont ( font , value )
1042+ pos = self .labelPosition ( value )
12351043 transform = self .labelTransformation (pos , labelSize )
12361044 br = transform .mapRect (QRectF (QPointF (0 , 0 ), labelSize ))
12371045 br .translate (- pos .x (), - pos .y ())
@@ -1411,3 +1219,62 @@ def updateMap(self):
14111219 sm .setPaintInterval (pos .y () + len_ , pos .y ())
14121220 else :
14131221 sm .setPaintInterval (pos .x (), pos .x () + len_ )
1222+
1223+
1224+ class QwtDateTimeScaleDraw (QwtScaleDraw ):
1225+ """Scale draw for datetime axis
1226+
1227+ This class formats axis labels as date/time strings from Unix timestamps.
1228+
1229+ Args:
1230+ format: Format string for datetime display (default: "%Y-%m-%d %H:%M:%S").
1231+ Uses Python datetime.strftime() format codes.
1232+ spacing: Spacing between labels (default: 4)
1233+
1234+ Examples:
1235+ >>> # Create a datetime scale with default format
1236+ >>> scale = QwtDateTimeScaleDraw()
1237+
1238+ >>> # Create a datetime scale with custom format (time only)
1239+ >>> scale = QwtDateTimeScaleDraw(format="%H:%M:%S")
1240+
1241+ >>> # Create a datetime scale with date only
1242+ >>> scale = QwtDateTimeScaleDraw(format="%Y-%m-%d", spacing=4)
1243+ """
1244+
1245+ def __init__ (self , format : str = "%Y-%m-%d %H:%M:%S" , spacing : int = 4 ) -> None :
1246+ super ().__init__ ()
1247+ self ._format = format
1248+ self .setSpacing (spacing )
1249+
1250+ def get_format (self ) -> str :
1251+ """Get the current datetime format string
1252+
1253+ Returns:
1254+ str: Format string
1255+ """
1256+ return self ._format
1257+
1258+ def set_format (self , format : str ) -> None :
1259+ """Set the datetime format string
1260+
1261+ Args:
1262+ format: Format string for datetime display
1263+ """
1264+ self ._format = format
1265+
1266+ def label (self , value : float ) -> QwtText :
1267+ """Convert a timestamp value to a formatted date/time label
1268+
1269+ Args:
1270+ value: Unix timestamp (seconds since epoch)
1271+
1272+ Returns:
1273+ QwtText: Formatted label
1274+ """
1275+ try :
1276+ dt = datetime .fromtimestamp (value )
1277+ return QwtText (dt .strftime (self ._format ))
1278+ except (ValueError , OSError ):
1279+ # Handle invalid timestamps
1280+ return QwtText ("" )
0 commit comments