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