Example #1
0
class AmzHistoryChart(QWidget):
    """A chart that graphs the history of an AmazonListing's sales rank, price, and number of offers."""

    def __init__(self, parent=None):
        super(AmzHistoryChart, self).__init__(parent=parent)

        self.dbsession = Session()
        self.context_menu_actions = []
        self._avg_pointspan = 0
        self._max_points = 100
        self.source = None
        self.history = None

        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

        # Set up the chart
        self.chart_view = QChartView(self)
        self.chart_view.setRenderHint(QPainter.Antialiasing)
        self.chart_view.setContextMenuPolicy(Qt.CustomContextMenu)
        self.chart_view.customContextMenuRequested.connect(self.context_menu)

        self.chart = QChart()
        self.chart.legend().hide()
        self.chart.setFlags(QGraphicsItem.ItemIsFocusable | QGraphicsItem.ItemIsSelectable)
        self.chart.installEventFilter(self)
        self.chart_view.setChart(self.chart)

        self.layout().addWidget(self.chart_view)

        # Create the axes
        rcolor = QColor(50, 130, 220)
        pcolor = QColor(0, 200, 0)
        ocolor = QColor(255, 175, 0)

        self.timeAxis = QDateTimeAxis()
        self.timeAxis.setFormat('M/dd hh:mm')
        self.timeAxis.setTitleText('Date/Time')
        self.chart.addAxis(self.timeAxis, Qt.AlignBottom)

        self.timeAxis.minChanged.connect(self.on_timeaxis_min_changed)

        self.rankAxis = QValueAxis()
        self.rankAxis.setLabelFormat('%\'i')
        self.rankAxis.setTitleText('Sales Rank')
        self.rankAxis.setLinePenColor(rcolor)
        self.rankAxis.setLabelsColor(rcolor)
        self.chart.addAxis(self.rankAxis, Qt.AlignLeft)

        self.priceAxis = QValueAxis()
        self.priceAxis.setLabelFormat('$%.2f')
        self.priceAxis.setTitleText('Price')
        self.priceAxis.setLinePenColor(pcolor)
        self.priceAxis.setLabelsColor(pcolor)
        self.chart.addAxis(self.priceAxis, Qt.AlignRight)

        # Create the series
        self.rankLine = QLineSeries()
        self.chart.addSeries(self.rankLine)
        self.rankLine.attachAxis(self.timeAxis)
        self.rankLine.attachAxis(self.rankAxis)
        self.rankLine.setColor(rcolor)

        self.priceLine = QLineSeries()
        self.chart.addSeries(self.priceLine)
        self.priceLine.attachAxis(self.timeAxis)
        self.priceLine.attachAxis(self.priceAxis)
        self.priceLine.setColor(pcolor)

        self.salesPoints = QScatterSeries()
        self.chart.addSeries(self.salesPoints)
        self.salesPoints.attachAxis(self.timeAxis)
        self.salesPoints.attachAxis(self.rankAxis)
        self.salesPoints.setColor(ocolor)

    def add_context_action(self, action):
        """Add an action to the chart's context menu."""
        self.context_menu_actions.append(action)

    def add_context_actions(self, actions):
        """Adds all action in an iterable."""
        self.context_menu_actions.extend(actions)

    def remove_context_action(self, action):
        """Removes an action from the chart's context menu."""
        self.context_menu_actions.remove(action)

    def context_menu(self, point):
        """Show a context menu on the chart."""
        menu = QMenu(self)
        menu.addActions(self.context_menu_actions)

        point = self.chart_view.viewport().mapToGlobal(point)
        menu.popup(point)

    def set_source(self, source):
        """Set the source listing for the graph."""
        self.source = source

        # Update the chart
        self.rankLine.clear()
        self.priceLine.clear()
        self.salesPoints.clear()
        self.history = None

        start_date = datetime.utcnow() - timedelta(days=5)
        self.load_history_from(start_date)

        self.reset_axes()

    def load_history_from(self, start_date=datetime.utcfromtimestamp(0)):
        """Load history data from start-present."""
        if not self.source:
            self._avg_pointspan = 0
            return

        # Get the earliest point already in the chart
        points = self.rankLine.pointsVector()

        if points:
            # The chart is drawn right-to-left, so the last point is the earliest point
            earliest_msecs = points[-1].x()
            earliest = datetime.fromtimestamp(earliest_msecs / 1000, timezone.utc)

            if earliest <= start_date:
                return

        else:
            earliest = datetime.now(timezone.utc)

        # Get the product history stats if we don't already have them
        if self.history is None:
            self.history = dbhelpers.ProductHistoryStats(self.dbsession, self.source.id)

        # Start adding points to the chart
        last_row = None
        for row in self.dbsession.query(AmzProductHistory).\
                                  filter(AmzProductHistory.amz_listing_id == self.source.id,
                                         AmzProductHistory.timestamp > start_date.replace(tzinfo=None),
                                         AmzProductHistory.timestamp < earliest.replace(tzinfo=None)).\
                                  order_by(AmzProductHistory.timestamp.desc()):

            # SqlAlchemy returns naive timestamps
            time = row.timestamp.replace(tzinfo=timezone.utc).timestamp() * 1000

            self.rankLine.append(time, row.salesrank or 0)
            self.priceLine.append(time, row.price or 0)

            if last_row:
                # It's possible for salesrank to be None
                try:
                    slope = (last_row.salesrank - row.salesrank) / (last_row.timestamp.timestamp() - row.timestamp.timestamp())
                    if slope < -0.3:
                        self.salesPoints.append(last_row.timestamp.replace(tzinfo=timezone.utc).timestamp() * 1000,
                                                last_row.salesrank)
                except (TypeError, AttributeError):
                    pass

            last_row = row

        # Calculate the average span between points
        spans = 0
        for p1, p2 in itertools.zip_longest(itertools.islice(points, 0, None, 2), itertools.islice(points, 1, None, 2)):
            if p1 and p2: spans += abs(p1.x() - p2.x())

        self._avg_pointspan = spans // 2

    def on_timeaxis_min_changed(self, min):
        """Respond to a change in the time axis' minimum value."""
        # toTime_t() converts to UTC automatically
        utc_min = datetime.fromtimestamp(min.toTime_t(), timezone.utc)
        self.load_history_from(start_date=utc_min - timedelta(days=1))

    def reset_axes(self):
        """Resets the chart axes."""
        r = self.rankLine.pointsVector()
        p = self.priceLine.pointsVector()

        # If there is only one data point, set the min and max to the day before and the day after
        if len(r) == 1:
            tmin = QDateTime.fromMSecsSinceEpoch(r[0].x(), Qt.LocalTime).addDays(-1)
            tmax = QDateTime.fromMSecsSinceEpoch(r[0].x(), Qt.LocalTime).addDays(+1)
        else:
            tmin = min(r, key=lambda pt: pt.x(), default=QPointF(QDateTime.currentDateTime().addDays(-1).toMSecsSinceEpoch(), 0)).x()
            tmax = max(r, key=lambda pt: pt.x(), default=QPointF(QDateTime.currentDateTime().addDays(+1).toMSecsSinceEpoch(), 0)).x()
            tmin = QDateTime.fromMSecsSinceEpoch(tmin, Qt.LocalTime)
            tmax = QDateTime.fromMSecsSinceEpoch(tmax, Qt.LocalTime)

        self.timeAxis.setMin(tmin)
        self.timeAxis.setMax(tmax)

        # Find the min and max values of the series
        min_point = lambda pts: min(pts, key=lambda pt: pt.y(), default=QPointF(0, 0))
        max_point = lambda pts: max(pts, key=lambda pt: pt.y(), default=QPointF(0, 0))

        rmin = min_point(r)
        rmax = max_point(r)
        pmin = min_point(p)
        pmax = max_point(p)

        # Scale the mins and maxes to 'friendly' values
        scalemin = lambda v, step: ((v - step / 2) // step) * step
        scalemax = lambda v, step: ((v + step / 2) // step + 1) * step

        # The the axis bounds

        rmin = max(scalemin(rmin.y(), 1000), 0)
        rmax = scalemax(rmax.y(), 1000)
        pmin = max(scalemin(pmin.y(), 5), 0)
        pmax = scalemax(pmax.y(), 5)

        self.rankAxis.setMin(rmin)
        self.rankAxis.setMax(rmax)
        self.priceAxis.setMin(pmin)
        self.priceAxis.setMax(pmax)

    def eventFilter(self, watched, event):
        """Intercept and handle mouse events."""
        if event.type() == QEvent.GraphicsSceneWheel and event.orientation() == Qt.Vertical:
            factor = 0.95 if event.delta() < 0 else 1.05
            self.chart.zoom(factor)
            return True

        if event.type() == QEvent.GraphicsSceneMouseDoubleClick:
            self.chart.zoomReset()
            self.reset_axes()
            return True

        if event.type() == QEvent.GraphicsSceneMouseMove:
            delta = event.pos() - event.lastPos()
            self.chart.scroll(-delta.x(), delta.y())
            return True

        return False
Example #2
0
class Chart(QChartView):
    def __init__(self):
        self.chart = QChart()

        QChartView.__init__(self, self.chart)

        self.axisX = QValueAxis()
        self.axisY = QValueAxis()

        self.xRange = [0, 100]
        self.yRange = [-100, 100]

        self.curve = []
        self.scatterCurve = []

        self.setBackgroundBrush(QColor('#D8D8D8'))

        self.setRenderHint(QPainter.Antialiasing)
        self.chart.setAnimationOptions(QChart.NoAnimation)
        self.chart.legend().setVisible(True)
        self.chart.legend().setAlignment(Qt.AlignBottom)
        self.chart.legend().setLabelColor(Qt.white)
        self.chart.legend().setMarkerShape(QLegend.MarkerShapeFromSeries)
        self.chart.setBackgroundBrush(QColor('#00004D'))

        penAxisGrid = QPen(QColor('#F2F2F2'))
        penAxisGrid.setWidthF(0.5)
        penAxisMinorGrid = QPen(QColor('#A4A4A4'))
        penAxisMinorGrid.setWidthF(0.3)

        self.axisX.setGridLinePen(penAxisGrid)
        self.axisX.setLinePen(penAxisGrid)
        self.axisY.setGridLinePen(penAxisGrid)
        self.axisY.setLinePen(penAxisGrid)

        self.axisX.setMinorGridLinePen(penAxisMinorGrid)
        self.axisY.setMinorGridLinePen(penAxisMinorGrid)

        self.axisX.setLabelsColor(Qt.white)
        self.axisY.setLabelsColor(Qt.white)

        self.axisX.setMinorTickCount(4)
        self.axisY.setMinorTickCount(4)

        self.axisX.setTitleBrush(Qt.white)
        self.axisY.setTitleBrush(Qt.white)

        self.axisX.setTitleText('Time')
        self.axisY.setTitleText('Signal Amplitude')

        self.axisX.setTickCount(11)
        self.axisY.setTickCount(11)
        self.axisX.setRange(self.xRange[0], self.xRange[1])
        self.axisY.setRange(self.yRange[0], self.yRange[1])

    def setAddSerie(self, name, color):
        self.curve.append(QLineSeries())
        pen = self.curve[len(self.curve) - 1].pen()

        pen.setColor(QColor(color))
        pen.setWidthF(2)
        self.curve[len(self.curve) - 1].setPen(pen)

        self.curve[len(self.curve) - 1].setName(name)

        self.chart.addSeries(self.curve[len(self.curve) - 1])

        self.chart.setAxisX(self.axisX, self.curve[len(self.curve) - 1])
        self.chart.setAxisY(self.axisY, self.curve[len(self.curve) - 1])

    def setAddScatterSerie(self, color):
        self.scatterCurve.append(QScatterSeries())

        pen = self.scatterCurve[len(self.scatterCurve) - 1].pen()
        pen.setColor(QColor(color))
        pen.setWidthF(1)
        self.scatterCurve[len(self.scatterCurve) - 1].setPen(pen)
        self.scatterCurve[len(self.scatterCurve) - 1].setColor(QColor(color))
        self.scatterCurve[len(self.scatterCurve) - 1].setMarkerSize(10)

        self.chart.addSeries(self.scatterCurve[len(self.scatterCurve) - 1])

        self.chart.setAxisX(self.axisX,
                            self.scatterCurve[len(self.scatterCurve) - 1])
        self.chart.setAxisY(self.axisY,
                            self.scatterCurve[len(self.scatterCurve) - 1])

    def setDataChartScatter(self, xData, yData1, yData2):
        if xData > self.xRange[1]:
            addValue = xData - self.xRange[1]

            if self.xRange[0] is not 0:
                self.xRange[0] = self.xRange[0] + addValue

            self.xRange[1] = self.xRange[1] + addValue
            self.axisX.setRange(self.xRange[0], self.xRange[1])

        self.curve[0].append(xData, yData1)
        self.curve[1].append(xData, yData2)
        self.scatterCurve[0].append(xData, yData1)
        self.scatterCurve[1].append(xData, yData2)

    def setDataChart(self, xData, yData1, yData2):
        if xData > self.xRange[1]:
            addValue = xData - self.xRange[1]

            if self.xRange[0] is not 0:
                self.xRange[0] = self.xRange[0] + addValue

            self.xRange[1] = self.xRange[1] + addValue
            self.axisX.setRange(self.xRange[0], self.xRange[1])

        self.curve[0].append(xData, yData1)
        self.curve[1].append(xData, yData2)

    def setRangeY(self, yRange, autoscale):
        if autoscale:

            if yRange[0] == 0 and yRange[1] == 0:
                yRange[0] = -0.50
                yRange[1] = 0.50

            elif yRange[0] == 0:
                yRange[0] = -0.10
                yRange[1] = yRange[1] * 1.10

            elif yRange[1] == 0:
                yRange[0] = yRange[0] * 1.10
                yRange[1] = 0.10

            else:

                if yRange[0] < 0:
                    yRange[0] = yRange[0] * 1.10

                else:
                    yRange[0] = yRange[0] - (yRange[0] * 0.10)

                if yRange[1] < 0:
                    yRange[1] = yRange[1] - (yRange[1] * 0.10)

                else:
                    yRange[1] = yRange[1] * 1.10

            self.axisY.setRange(yRange[0], yRange[1])

        else:
            self.axisY.setRange(yRange[0], yRange[1])

    def setRangeX(self, xRange):
        self.axisX.setRange(xRange[0], xRange[1])

    def getRangeX(self):
        return self.xRange

    def setAxisXName(self, name):
        self.axisX.setTitleText(name)

    def setAxisYName(self, name):
        self.axisY.setTitleText(name)

    def setAxisXTickCount(self, tick):
        self.axisX.setTickCount(tick)

    def setAxisYTickCount(self, tick):
        self.axisY.setTickCount(tick)

    def initSerie(self):
        self.curve.clear()

    def initSeries(self):
        self.curve[0].clear()
        self.curve[1].clear()