def setupChart(self): """Set up the GUI's series and chart.""" # Collect x and y data values from the CSV file x_values, y_values = self.loadCSVFile() # Get the largest x and y values; Used for setting the chart's axes x_max, y_max = max(x_values), max(y_values) # Create numpy arrays from the x and y values x_values = np.array(x_values) y_values = np.array(y_values) # Calculate the regression line coefficients = linearRegression(x_values, y_values) # Create chart object chart = QChart() chart.setTitle("Auto Insurance for Geographical Zones in Sweden") chart.legend().hide() # Create scatter series and add points to the series scatter_series = QScatterSeries() scatter_series.setName("DataPoints") scatter_series.setMarkerSize(9.0) scatter_series.hovered.connect(self.displayPointInfo) for value in range(0, self.row_count - 1): scatter_series.append(x_values[value], y_values[value]) scatter_series.setBorderColor(QColor('#000000')) # Create line series and add points to the series line_series = QLineSeries() line_series.setName("RegressionLine") # Calculate the regression line for x in x_values: y_pred = coefficients[0] + coefficients[1] * x line_series.append(x, y_pred) # Add both series to the chart and create x and y axes chart.addSeries(scatter_series) chart.addSeries(line_series) chart.createDefaultAxes() axis_x = chart.axes(Qt.Horizontal) axis_x[0].setTitleText("Number of Claims") axis_x[0].setRange(0, x_max) axis_x[0].setLabelFormat("%i") axis_y = chart.axes(Qt.Vertical) axis_y[0].setTitleText( "Total Payment in Swedish Kronor (in thousands)") axis_y[0].setRange(0, y_max + 20) # Create QChartView object for displaying the chart chart_view = QChartView(chart) v_box = QVBoxLayout() v_box.addWidget(chart_view) self.setLayout(v_box)
def main(): import sys app = QApplication(sys.argv) acmeSeries = QBoxPlotSeries() acmeSeries.setName("Acme Ltd") boxWhiskSeries = QBoxPlotSeries() boxWhiskSeries.setName("BoxWhisk Inc") acmeData = QFile(":acme") if not acmeData.open(QIODevice.ReadOnly | QIODevice.Text): sys.exit(1) dataReader = BoxDataReader(acmeData) while not dataReader.atEnd(): _set = dataReader.readBox() if _set is not None: acmeSeries.append(_set) boxwhiskData = QFile(":boxwhisk") if not boxwhiskData.open(QIODevice.ReadOnly | QIODevice.Text): sys.exit(1) dataReader.readFile(boxwhiskData) while not dataReader.atEnd(): _set = dataReader.readBox() if _set is not None: boxWhiskSeries.append(_set) chart = QChart() chart.addSeries(acmeSeries) chart.addSeries(boxWhiskSeries) chart.setTitle("Acme Ltd and BoxWhisk Inc share deviation in 2012") chart.setAnimationOptions(QChart.SeriesAnimations) chart.createDefaultAxes() chart.axes(Qt.Vertical)[0].setMin(15.0) chart.axes(Qt.Vertical)[0].setMax(34.0) chart.legend().setVisible(True) chart.legend().setAlignment(Qt.AlignBottom) chartView = QChartView(chart) chartView.setRenderHint(QPainter.Antialiasing) window = QMainWindow() window.setCentralWidget(chartView) window.resize(800, 600) window.show() sys.exit(app.exec_())
def main(): import sys a = QApplication(sys.argv) acmeSeries = QCandlestickSeries() acmeSeries.setName("Acme Ltd") acmeSeries.setIncreasingColor(QColor(Qt.green)) acmeSeries.setDecreasingColor(QColor(Qt.red)) acmeData = QFile(":acme") if not acmeData.open(QIODevice.ReadOnly | QIODevice.Text): sys.exit(1) categories = [] dataReader = CandlestickDataReader(acmeData) while not dataReader.atEnd(): _set = dataReader.readCandlestickSet() if _set is not None: acmeSeries.append(_set) categories.append( QDateTime.fromMSecsSinceEpoch(int( _set.timestamp())).toString("dd")) chart = QChart() chart.addSeries(acmeSeries) chart.setTitle("Acme Ltd Historical Data (July 2015)") chart.setAnimationOptions(QChart.SeriesAnimations) chart.createDefaultAxes() axisX = chart.axes(Qt.Horizontal)[0] axisX.setCategories(categories) axisY = chart.axes(Qt.Vertical)[0] axisY.setMax(axisY.max() * 1.01) axisY.setMin(axisY.min() * 0.99) chart.legend().setVisible(True) chart.legend().setAlignment(Qt.AlignBottom) chartView = QChartView(chart) chartView.setRenderHint(QPainter.Antialiasing) window = QMainWindow() window.setCentralWidget(chartView) window.resize(800, 600) window.show() sys.exit(a.exec_())
def main(): import sys app = QApplication(sys.argv) series0 = QLineSeries() series1 = QLineSeries() series0 << QPointF(1, 5) << QPointF(3, 7) << QPointF(7, 6) << QPointF( 9, 7) << QPointF(12, 6) << QPointF(16, 7) << QPointF(18, 5) series1 << QPointF(1, 3) << QPointF(3, 4) << QPointF(7, 3) << QPointF( 8, 2) << QPointF(12, 3) << QPointF(16, 4) << QPointF(18, 3) series = QAreaSeries(series0, series1) series.setName("Batman") pen = QPen(0x059605) pen.setWidth(3) series.setPen(pen) gradient = QLinearGradient(QPointF(0, 0), QPointF(0, 1)) gradient.setColorAt(0.0, QColor(0x3CC63C)) gradient.setColorAt(1.0, QColor(0x26F626)) gradient.setCoordinateMode(QGradient.ObjectBoundingMode) series.setBrush(gradient) chart = QChart() chart.addSeries(series) chart.setTitle("Simple areachart example") chart.createDefaultAxes() chart.axes(Qt.Horizontal)[0].setRange(0, 20) chart.axes(Qt.Vertical)[0].setRange(0, 10) chartView = QChartView(chart) chartView.setRenderHint(QPainter.Antialiasing) window = QMainWindow() window.setCentralWidget(chartView) window.resize(400, 300) window.show() sys.exit(app.exec_())
class StartWindow(QMainWindow): def __init__(self): super(StartWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.samplingRate = 0 self.continueLoop = True self.chart = QChart() self.chart.setTitle('pressureGraph') self.chartView = QChartView(self.chart) self.ui.verticalLayout.addWidget(self.chartView) self.ui.samplingButton.clicked.connect(self.getSamplingRate) self.ui.resetButton.clicked.connect(self.startCollection) self.ui.stopButton.clicked.connect(self.stopLoop) def getSamplingRate(self): self.samplingRate = float(self.ui.frequencyEdit.text()) print(self.samplingRate) def stopLoop(self): self.continueLoop = False def setAxis(self, time_passed, yList): horAxis = self.chart.axes(orientation=Qt.Horizontal) verAxis = self.chart.axes(orientation=Qt.Vertical) max_time = time_passed / self.samplingRate # set axis limits horAxis[0].setMax(max_time) horAxis[0].setMin(max(max_time - 10, 0)) verAxis[0].setMax(max(yList)) verAxis[0].setMin(min(yList)) def process(self, series): time_passed = 0 pList = list() while self.continueLoop: # to be able to get mouse input QApplication.processEvents() cur_pressure = dummy_pressure.get_pressure() print(cur_pressure) if len(series) != 0: self.chart.removeSeries(series) if len(series) > 10: # remove the first index after certain number of data series.remove(0) pList = pList[1:] series.append(time_passed / self.samplingRate, cur_pressure) pList.append(cur_pressure) self.chart.addSeries(series) # only create new axes in first instance to avoid clipping of labels. if len(series) < 2: self.chart.createDefaultAxes() self.ui.pressureDisplay.setText('{:.4f}'.format(cur_pressure)) leak = False leakO = leak_check() leak = leakO.check_leak(cur_pressure) if not leak: self.ui.leakStatusBox.setText("No Pressure Drop detected") else: self.ui.leakStatusBox.setText( "Pressure drop detected. Email sent") email_warning.send_message(pList) return self.setAxis(time_passed, pList) time.sleep(1 / self.samplingRate) #self.chart.removeAxis(horAxis[0]) #self.chart.removeAxis(verAxis[0]) time_passed += 1 return def startCollection(self): series = QLineSeries() if self.samplingRate == 0: msgBox = QMessageBox() msgBox.setText('Set Sampling rate first') msgBox.exec() return self.continueLoop = True self.process(series) # t1 = threading.Thread(target=self.process, args = (series)) print('stopped') return
class QtAgentGraph(QChartView): def __init__(self, spec): super().__init__(None) self.spec = spec self.chart = QChart() self.chart.setTitle(str(self.spec.variable)) self.chart.legend().hide() self.setMinimumWidth(400) self.setMinimumHeight(230) self.setChart(self.chart) self.setRenderHint(QPainter.Antialiasing) self.chart.createDefaultAxes() self.autoscale_y_axis = True if self.spec.min_y and self.spec.max_y: self.autoscale_y_axis = False self.chart.axes()[1].setRange(self.spec.min_y, self.spec.max_y) self.axis_x = QValueAxis() self.axis_y = QValueAxis() self.chart.addAxis(self.axis_x, Qt.AlignBottom) self.chart.addAxis(self.axis_y, Qt.AlignLeft) self._updates_per_second = 60 self._data = [] self._min = 0 self._max = 0 def clear(self): for chart in self.chart.series(): chart.clear() self._data = [] def update_data(self): for a in self.spec.agents: if not hasattr(a, "_agent_series"): a._agent_series = QLineSeries() a._agent_series.setColor( QColor(a.color[0], a.color[1], a.color[2])) a._agent_series_data = [getattr(a, self.spec.variable)] self.chart.addSeries(a._agent_series) a._agent_series.attachAxis(self.chart.axisX()) a._agent_series.attachAxis(self.chart.axisY()) else: a._agent_series_data.append(getattr(a, self.spec.variable)) def redraw(self): for a in self.spec.agents: if hasattr(a, "_agent_series") and len(a._agent_series_data) > 0: datapoint = sum(a._agent_series_data) / len( a._agent_series_data) a._agent_series.append( QPointF( a._agent_series.count() / self._updates_per_second, datapoint, )) self._min = min(self._min, datapoint) self._max = max(self._max, datapoint) a._agent_series.setColor( QColor(a.color[0], a.color[1], a.color[2])) a._agent_series_data = [] if len(self.spec.agents) > 0: first_agent = self.spec.agents[0] if hasattr(first_agent, "_agent_series"): first_series = first_agent._agent_series self.chart.axes()[0].setRange(0, (first_series.count() - 1) / self._updates_per_second) diff = self._max - self._min if self.autoscale_y_axis: if diff > 0: self.chart.axes()[1].setRange(self._min, self._max) else: self.chart.axes()[1].setRange(self._min - 0.5, self._max + 0.5)
class QtGraph(QChartView): def __init__(self, spec): super().__init__(None) self.spec = spec self.chart = QChart() # self.chart.setTitle(str(self.spec.variables)) # self.chart.legend().hide() for i in range(len(self.spec.variables)): series = QLineSeries() series.setColor( QColor( self.spec.colors[i][0], self.spec.colors[i][1], self.spec.colors[i][2], )) series.setName(self.spec.variables[i]) self.chart.addSeries(series) self.setMinimumWidth(400) self.setMinimumHeight(230) self.setChart(self.chart) self.setRenderHint(QPainter.Antialiasing) self.chart.createDefaultAxes() self.autoscale_y_axis = True if self.spec.min_y and self.spec.max_y: self.autoscale_y_axis = False self.chart.axes()[1].setRange(self.spec.min_y, self.spec.max_y) self._updates_per_second = 60 self._data = [] self._min = 0 self._max = 0 def clear(self): for chart in self.chart.series(): chart.clear() self._data = [] def add_data(self, data): self._data.append(data) def redraw(self): if len(self._data) > 0: for i in range(len(self.spec.variables)): data = [datapoint[i] for datapoint in self._data] datapoint = sum(data) / len(data) self.chart.series()[i].append( QPointF( self.chart.series()[i].count() / self._updates_per_second, datapoint, )) self._min = min(self._min, datapoint) self._max = max(self._max, datapoint) self.chart.axes()[0].setRange(0, (self.chart.series()[0].count() - 1) / self._updates_per_second) diff = self._max - self._min if self.autoscale_y_axis: if diff > 0: self.chart.axes()[1].setRange(self._min, self._max) else: self.chart.axes()[1].setRange(self._min - 0.5, self._max + 0.5) self._data = []
class IMUChartView(QChartView, BaseGraph): def __init__(self, parent=None): super().__init__(parent=parent) # Render on OpenGL self.setViewport(QOpenGLWidget()) self.xvalues = {} self.chart = QChart() self.setChart(self.chart) self.chart.legend().setVisible(True) self.chart.legend().setAlignment(Qt.AlignTop) self.ncurves = 0 self.setRenderHint(QPainter.Antialiasing) # Cursor (with chart as parent) self.cursor = QGraphicsLineItem(self.chart) self.cursor.setZValue(100.0) # self.scene().addItem(self.cursor) # Selection features # self.setRubberBand(QChartView.HorizontalRubberBand) self.selectionBand = QRubberBand(QRubberBand.Rectangle, self) self.selecting = False self.initialClick = QPoint() # Track mouse self.setMouseTracking(True) self.labelValue = QLabel(self) self.labelValue.setStyleSheet( "background-color: rgba(255,255,255,75%); color: black;") self.labelValue.setAlignment(Qt.AlignCenter) self.labelValue.setMargin(5) self.labelValue.setVisible(False) self.build_style() self.selection_start_time = None self.selection_stop_time = None self.cursor_time = None def build_style(self): self.setStyleSheet("QLabel{color:blue;}") self.chart.setTheme(QChart.ChartThemeBlueCerulean) self.setBackgroundBrush(QBrush(Qt.darkGray)) self.chart.setPlotAreaBackgroundBrush(QBrush(Qt.black)) self.chart.setPlotAreaBackgroundVisible(True) def save_as_png(self, file_path): pixmap = self.grab() child = self.findChild(QOpenGLWidget) painter = QPainter(pixmap) if child is not None: d = child.mapToGlobal(QPoint()) - self.mapToGlobal(QPoint()) painter.setCompositionMode(QPainter.CompositionMode_SourceAtop) painter.drawImage(d, child.grabFramebuffer()) painter.end() pixmap.save(file_path, 'PNG') # def closeEvent(self, event): # self.aboutToClose.emit(self) @staticmethod def decimate(xdata, ydata): # assert(len(xdata) == len(ydata)) # Decimate only if we have too much data decimate_factor = len(xdata) / 100000.0 # decimate_factor = 1.0 if decimate_factor > 1.0: decimate_factor = int(np.floor(decimate_factor)) # print('decimate factor', decimate_factor) x = np.ndarray(int(len(xdata) / decimate_factor), dtype=np.float64) y = np.ndarray(int(len(ydata) / decimate_factor), dtype=np.float64) # for i in range(len(x)): for i, _ in enumerate(x): index = i * decimate_factor # assert(index < len(xdata)) x[i] = xdata[index] y[i] = ydata[index] if x[i] < x[0]: print('timestamp error', x[i], x[0]) return x, y else: return xdata, ydata @pyqtSlot(float, float) def axis_range_changed(self, min_value, max_value): # print('axis_range_changed', min, max) for axis in self.chart.axes(): axis.applyNiceNumbers() def update_axes(self): # Get and remove all axes for axis in self.chart.axes(): self.chart.removeAxis(axis) # Create new axes # Create axis X axisx = QDateTimeAxis() axisx.setTickCount(5) axisx.setFormat("dd MMM yyyy hh:mm:ss") axisx.setTitleText("Date") self.chart.addAxis(axisx, Qt.AlignBottom) # Create axis Y axisY = QValueAxis() axisY.setTickCount(5) axisY.setLabelFormat("%.3f") axisY.setTitleText("Values") self.chart.addAxis(axisY, Qt.AlignLeft) # axisY.rangeChanged.connect(self.axis_range_changed) ymin = None ymax = None # Attach axes to series, find min-max for series in self.chart.series(): series.attachAxis(axisx) series.attachAxis(axisY) vect = series.pointsVector() # for i in range(len(vect)): for i, _ in enumerate(vect): if ymin is None: ymin = vect[i].y() ymax = vect[i].y() else: ymin = min(ymin, vect[i].y()) ymax = max(ymax, vect[i].y()) # Update range # print('min max', ymin, ymax) if ymin is not None: axisY.setRange(ymin, ymax) # Make the X,Y axis more readable # axisx.applyNiceNumbers() # axisY.applyNiceNumbers() def add_data(self, xdata, ydata, color=None, legend_text=None): curve = QLineSeries() pen = curve.pen() if color is not None: pen.setColor(color) pen.setWidthF(1.5) curve.setPen(pen) # curve.setPointsVisible(True) # curve.setUseOpenGL(True) self.total_samples = max(self.total_samples, len(xdata)) # Decimate xdecimated, ydecimated = self.decimate(xdata, ydata) # Data must be in ms since epoch # curve.append(self.series_to_polyline(xdecimated * 1000.0, ydecimated)) # self.reftime = datetime.datetime.fromtimestamp(xdecimated[0]) # if len(xdecimated) > 0: # xdecimated = xdecimated - xdecimated[0] xdecimated *= 1000 # No decimal expected points = [] for i, _ in enumerate(xdecimated): # TODO hack # curve.append(QPointF(xdecimated[i], ydecimated[i])) points.append(QPointF(xdecimated[i], ydecimated[i])) curve.replace(points) points.clear() if legend_text is not None: curve.setName(legend_text) # Needed for mouse events on series self.chart.setAcceptHoverEvents(True) self.xvalues[self.ncurves] = np.array(xdecimated) # connect signals / slots # curve.clicked.connect(self.lineseries_clicked) # curve.hovered.connect(self.lineseries_hovered) # Add series self.chart.addSeries(curve) self.ncurves += 1 self.update_axes() def update_data(self, xdata, ydata, series_id): if series_id < len(self.chart.series()): # Find start time to replace data current_series = self.chart.series()[series_id] current_points = current_series.pointsVector() # Find start and end indexes start_index = -1 try: start_index = current_points.index( QPointF(xdata[0] * 1000, ydata[0])) # Right on the value! # print("update_data: start_index found exact match.") except ValueError: # print("update_data: start_index no exact match - scanning deeper...") for i, value in enumerate(current_points): # print(str(current_points[i].x()) + " == " + str(xdata[0]*1000)) if current_points[i].x() == xdata[0] * 1000 or ( i > 0 and current_points[i - 1].x() < xdata[0] * 1000 < current_points[i].x()): start_index = i # print("update_data: start_index found approximative match.") break end_index = -1 try: end_index = current_points.index( QPointF(xdata[len(xdata) - 1] * 1000, ydata[len(ydata) - 1])) # Right on! # print("update_data: start_index found exact match.") except ValueError: # print("update_data: start_index no exact match - scanning deeper...") for i, value in enumerate(current_points): # print(str(current_points[i].x()) + " == " + str(xdata[0]*1000)) if current_points[i].x( ) == xdata[len(xdata) - 1] * 1000 or ( i > 0 and current_points[i - 1].x() < xdata[len(xdata) - 1] * 1000 < current_points[i].x()): end_index = i # print("update_data: start_index found approximative match.") break if start_index < 0 or end_index < 0: return # Decimate, if needed xdata, ydata = self.decimate(xdata, ydata) # Check if we have the same number of points for that range. If not, remove and replace! target_points = current_points[start_index:end_index] if len(target_points) != len(xdata): points = [] for i, value in enumerate(xdata): # TODO improve points.append(QPointF(value * 1000, ydata[i])) new_points = current_points[0:start_index] + points[0:len(points)-1] + \ current_points[end_index:len(current_points)-1] current_series.replace(new_points) new_points.clear() # self.xvalues[series_id] = np.array(xdata) return @classmethod def set_title(cls, title): # print('Setting title: ', title) # self.chart.setTitle(title) return @staticmethod def series_to_polyline(xdata, ydata): """Convert series data to QPolygon(F) polyline This code is derived from PythonQwt's function named `qwt.plot_curve.series_to_polyline`""" # print('series_to_polyline types:', type(xdata[0]), type(ydata[0])) size = len(xdata) polyline = QPolygonF(size) # for i in range(0, len(xdata)): # polyline[i] = QPointF(xdata[i] - xdata[0], ydata[i]) for i, data in enumerate(xdata): polyline[i] = QPointF(data - xdata[0], ydata[i]) # pointer = polyline.data() # dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo # pointer.setsize(2*polyline.size()*tinfo(dtype).dtype.itemsize) # memory = np.frombuffer(pointer, dtype) # memory[:(size-1)*2+1:2] = xdata # memory[1:(size-1)*2+2:2] = ydata return polyline def add_test_data(self): # 100Hz, one day accelerometer values npoints = 1000 * 60 * 24 xdata = np.linspace(0., 10., npoints) self.add_data(xdata, np.sin(xdata), color=Qt.red, legend_text='Acc. X') # self.add_data(xdata, np.cos(xdata), color=Qt.green, legend_text='Acc. Y') # self.add_data(xdata, np.cos(2 * xdata), color=Qt.blue, legend_text='Acc. Z') self.set_title( "Simple example with %d curves of %d points (OpenGL Accelerated Series)" % (self.ncurves, npoints)) def mouseMoveEvent(self, e: QMouseEvent): if self.selecting: clicked_x = max( self.chart.plotArea().x(), min( self.mapToScene(e.pos()).x(), self.chart.plotArea().x() + self.chart.plotArea().width())) clicked_y = max( self.chart.plotArea().y(), min( self.mapToScene(e.pos()).y(), self.chart.plotArea().y() + self.chart.plotArea().height())) current_pos = QPoint(clicked_x, clicked_y) # e.pos() self.selection_stop_time = self.chart.mapToValue( QPointF(clicked_x, 0)).x() self.setCursorPosition(clicked_x, True) if self.interaction_mode == GraphInteractionMode.SELECT: if current_pos.x() < self.initialClick.x(): start_x = current_pos.x() width = self.initialClick.x() - start_x else: start_x = self.initialClick.x() width = current_pos.x() - self.initialClick.x() self.selectionBand.setGeometry( QRect(start_x, self.chart.plotArea().y(), width, self.chart.plotArea().height())) if self.selection_rec: self.selection_rec.setRect( QRectF(start_x, self.chart.plotArea().y(), width, self.chart.plotArea().height())) if self.interaction_mode == GraphInteractionMode.MOVE: new_pos = current_pos - self.initialClick self.chart.scroll(-new_pos.x(), new_pos.y()) self.initialClick = current_pos def mousePressEvent(self, e: QMouseEvent): # Handling rubberbands # super().mousePressEvent(e) # Verify if click is inside plot area # if self.chart.plotArea().contains(e.pos()): self.selecting = True clicked_x = max( self.chart.plotArea().x(), min( self.mapToScene(e.pos()).x(), self.chart.plotArea().x() + self.chart.plotArea().width())) clicked_y = max( self.chart.plotArea().y(), min( self.mapToScene(e.pos()).y(), self.chart.plotArea().y() + self.chart.plotArea().height())) self.initialClick = QPoint(clicked_x, clicked_y) # e.pos() self.selection_start_time = self.chart.mapToValue(QPointF( clicked_x, 0)).x() self.setCursorPosition(clicked_x, True) if self.interaction_mode == GraphInteractionMode.SELECT: self.selectionBand.setGeometry( QRect(self.initialClick.x(), self.chart.plotArea().y(), 1, self.chart.plotArea().height())) self.selectionBand.show() if self.interaction_mode == GraphInteractionMode.MOVE: QGuiApplication.setOverrideCursor(Qt.ClosedHandCursor) self.labelValue.setVisible(False) def mouseReleaseEvent(self, e: QMouseEvent): # Assure if click is inside plot area # Handling rubberbands (with min / max x) clicked_x = max( self.chart.plotArea().x(), min( self.mapToScene(e.pos()).x(), self.chart.plotArea().x() + self.chart.plotArea().width())) if self.interaction_mode == GraphInteractionMode.SELECT: self.selectionBand.hide() if clicked_x != self.mapToScene(self.initialClick).x(): mapped_x = self.mapToScene(self.initialClick).x() if self.initialClick.x() < clicked_x: self.setSelectionArea(mapped_x, clicked_x, True) else: self.setSelectionArea(clicked_x, mapped_x, True) if self.interaction_mode == GraphInteractionMode.MOVE: QGuiApplication.restoreOverrideCursor() self.selecting = False def clearSelectionArea(self, emit_signal=False): if self.selection_rec: self.scene().removeItem(self.selection_rec) self.selection_rec = None if emit_signal: self.clearedSelectionArea.emit() def setSelectionArea(self, start_pos, end_pos, emit_signal=False): selection_brush = QBrush(QColor(153, 204, 255, 128)) selection_pen = QPen(Qt.transparent) if self.selection_rec: self.scene().removeItem(self.selection_rec) self.selection_rec = self.scene().addRect( start_pos, self.chart.plotArea().y(), end_pos - start_pos, self.chart.plotArea().height(), selection_pen, selection_brush) if emit_signal: self.selectedAreaChanged.emit( self.chart.mapToValue(QPointF(start_pos, 0)).x(), self.chart.mapToValue(QPointF(end_pos, 0)).x()) def setSelectionAreaFromTime(self, start_time, end_time, emit_signal=False): # Convert times to x values if isinstance(start_time, datetime.datetime): start_time = start_time.timestamp() * 1000 if isinstance(end_time, datetime.datetime): end_time = end_time.timestamp() * 1000 start_pos = self.chart.mapToPosition(QPointF(start_time, 0)).x() end_pos = self.chart.mapToPosition(QPointF(end_time, 0)).x() self.setSelectionArea(start_pos, end_pos) def setCursorPosition(self, pos, emit_signal=False): self.cursor_time = self.chart.mapToValue(QPointF(pos, 0)).x() # print (pos) pen = self.cursor.pen() pen.setColor(Qt.cyan) pen.setWidthF(1.0) self.cursor.setPen(pen) # On Top self.cursor.setZValue(100.0) area = self.chart.plotArea() x = pos y1 = area.y() y2 = area.y() + area.height() # self.cursor.set self.cursor.setLine(x, y1, x, y2) self.cursor.show() xmap_initial = self.chart.mapToValue(QPointF(pos, 0)).x() display = '' # '<i>' + (datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp())).strftime('%d-%m-%Y %H:%M:%S') + # '</i><br />' ypos = 10 last_val = None for i in range(self.ncurves): # Find nearest point idx = (np.abs(self.xvalues[i] - xmap_initial)).argmin() ymap = self.chart.series()[i].at(idx).y() xmap = self.chart.series()[i].at(idx).x() if i == 0: display += "<i>" + (datetime.datetime.fromtimestamp(xmap_initial/1000)).strftime('%d-%m-%Y %H:%M:%S:%f') + \ "</i>" # Compute where to display label if last_val is None or ymap > last_val: last_val = ymap ypos = self.chart.mapToPosition(QPointF(xmap, ymap)).y() if display != '': display += '<br />' display += self.chart.series()[i].name( ) + ': <b>' + '%.3f' % ymap + '</b>' self.labelValue.setText(display) self.labelValue.setGeometry(pos, ypos, 100, 100) self.labelValue.adjustSize() self.labelValue.setVisible(True) if emit_signal: self.cursorMoved.emit(xmap_initial) self.update() def setCursorPositionFromTime(self, timestamp, emit_signal=False): # Find nearest point if isinstance(timestamp, datetime.datetime): timestamp = timestamp.timestamp() pos = self.get_pos_from_time(timestamp) self.setCursorPosition(pos, emit_signal) def get_pos_from_time(self, timestamp): px = timestamp idx1 = (np.abs(self.xvalues[0] - px)).argmin() x1 = self.chart.series()[0].at(idx1).x() pos1 = self.chart.mapToPosition(QPointF(x1, 0)).x() idx2 = idx1 + 1 if idx2 < len(self.chart.series()[0]): x2 = self.chart.series()[0].at(idx2).x() if x2 != x1: pos2 = self.chart.mapToPosition(QPointF(x2, 0)).x() x2 /= 1000 x1 /= 1000 pos = (((px - x1) / (x2 - x1)) * (pos2 - pos1)) + pos1 else: pos = pos1 else: pos = pos1 return pos def resizeEvent(self, e: QResizeEvent): super().resizeEvent(e) # oldSize = e.oldSize() # newSize = e.size() # Update cursor from time if self.cursor_time: self.setCursorPositionFromTime(self.cursor_time / 1000.0) # Update selection if self.selection_rec: self.setSelectionAreaFromTime(self.selection_start_time, self.selection_stop_time) def zoom_in(self): self.chart.zoomIn() self.update_axes() def zoom_out(self): self.chart.zoomOut() self.update_axes() def zoom_area(self): if self.selection_rec: zoom_rec = self.selection_rec.rect() zoom_rec.setY(0) zoom_rec.setHeight(self.chart.plotArea().height()) self.chart.zoomIn(zoom_rec) self.clearSelectionArea(True) self.update_axes() def zoom_reset(self): self.chart.zoomReset() self.update_axes() def get_displayed_start_time(self): min_x = self.chart.mapToScene(self.chart.plotArea()).boundingRect().x() xmap = self.chart.mapToValue(QPointF(min_x, 0)).x() return datetime.datetime.fromtimestamp(xmap / 1000) def get_displayed_end_time(self): max_x = self.chart.mapToScene(self.chart.plotArea()).boundingRect().x() max_x += self.chart.mapToScene( self.chart.plotArea()).boundingRect().width() xmap = self.chart.mapToValue(QPointF(max_x, 0)).x() return datetime.datetime.fromtimestamp(xmap / 1000) @property def is_zoomed(self): return self.chart.isZoomed()
class Statistics(QWidget): """A statistics widget that displays information about studied time, shows grown plants, etc...""" def __init__(self, history, *args, **kwargs): super().__init__(*args, **kwargs) self.history = history chart = self.generate_chart() self.SPACING = 10 self.MIN_WIDTH = 400 self.MIN_HEIGHT = 200 chart.setMinimumWidth(self.MIN_WIDTH) chart.setMinimumHeight(self.MIN_HEIGHT) image_layout = QVBoxLayout() self.plant_study = None # the study record the plant is a part of self.plant: Optional[Plant] = None # the plant being displayed self.plant_date_label = QLabel(self) self.plant_date_label.setAlignment(Qt.AlignLeft) self.plant_duration_label = QLabel(self) self.plant_duration_label.setAlignment(Qt.AlignRight) label_layout = QHBoxLayout() label_layout.addWidget(self.plant_date_label) label_layout.addWidget(self.plant_duration_label) self.canvas = Canvas(self) self.canvas.setMinimumWidth(self.MIN_HEIGHT) self.canvas.setMinimumHeight(self.MIN_HEIGHT) stacked_layout = QVBoxLayout() stacked_layout.addLayout(label_layout) stacked_layout.addWidget(self.canvas) image_control = QHBoxLayout() text_color = self.palette().text().color() self.age_slider = QSlider(Qt.Horizontal, minimum=0, maximum=1000, value=1000, valueChanged=self.slider_value_changed) self.left_button = QPushButton(self, clicked=self.left, icon=qtawesome.icon('fa5s.angle-left', color=text_color)) self.right_button = QPushButton(self, clicked=self.right, icon=qtawesome.icon('fa5s.angle-right', color=text_color)) self.save_button = QPushButton(self, clicked=self.save, icon=qtawesome.icon('fa5s.download', color=text_color)) image_control.addWidget(self.left_button) image_control.addWidget(self.right_button) image_control.addSpacing(self.SPACING) image_control.addWidget(self.age_slider) image_control.addSpacing(self.SPACING) image_control.addWidget(self.save_button) image_layout.addLayout(stacked_layout) image_layout.addLayout(image_control) image_layout.setContentsMargins(self.SPACING, self.SPACING, self.SPACING, self.SPACING) separator = QFrame() separator.setStyleSheet(f"background-color: {self.palette().text().color().name()}") separator.setFixedWidth(1) main_layout = QGridLayout() main_layout.setHorizontalSpacing(self.SPACING * 2) main_layout.setColumnStretch(0, 1) main_layout.setColumnStretch(2, 0) main_layout.addWidget(chart, 0, 0) main_layout.addWidget(separator, 0, 1) main_layout.addLayout(image_layout, 0, 2) self.setLayout(main_layout) self.move() # go to the most recent plant self.refresh() def generate_chart(self): """Generate the bar graph for the widget.""" self.tags = [QBarSet(tag) for tag in ["Study"]] series = QStackedBarSeries() for set in self.tags: series.append(set) self.chart = QChart() self.chart.addSeries(series) self.chart.setTitle("Total time studied (minutes per day)") days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] axis = QBarCategoryAxis() axis.append(days) self.chart.createDefaultAxes() self.chart.setAxisX(axis, series) self.chart.legend().setAlignment(Qt.AlignBottom) self.chart.legend().setVisible(False) self.chart.setTheme(QChart.ChartThemeQt) self.chart.setBackgroundVisible(False) self.chart.setBackgroundRoundness(0) self.chart.setMargins(QMargins(0, 0, 0, 0)) self.chart.setTitleBrush(QBrush(self.palette().text().color())) yAxis = self.chart.axes(Qt.Vertical)[0] yAxis.setGridLineVisible(False) yAxis.setLabelFormat("%d") yAxis.setLinePenColor(self.palette().text().color()) yAxis.setLabelsColor(self.palette().text().color()) xAxis = self.chart.axes(Qt.Horizontal)[0] xAxis.setGridLineVisible(False) xAxis.setLinePenColor(self.palette().text().color()) xAxis.setLabelsColor(self.palette().text().color()) chartView = QChartView(self.chart) chartView.setRenderHint(QPainter.Antialiasing) return chartView def slider_value_changed(self): """Called when the slider value has changed. Sets the age of the plant and updates it.""" if self.plant is not None: # makes it a linear function from 0 to whatever the duration was, so the plant appears to grow normally self.plant.set_age( self.plant.inverse_age_coefficient_function(self.age_slider.value() / self.age_slider.maximum() * self.plant.age_coefficient_function( self.plant_study["duration"]))) self.canvas.update() def refresh(self): """Refresh the labels.""" # clear tag values for tag in self.tags: tag.remove(0, tag.count()) study_minutes = [0] * 7 for study in self.history.get_studies(): # TODO: don't just crash study_minutes[study["date"].weekday()] += study["duration"] for minutes in study_minutes: self.tags[0] << minutes # manually set the range of the y axis, because it doesn't for some reason yAxis = self.chart.axes(Qt.Vertical)[0] yAxis.setRange(0, max(study_minutes)) def left(self): """Move to the left (older) plant.""" self.move(-1) def right(self): """Move to the right (newer) plant.""" self.move(1) def move(self, delta: int = 0): """Move to the left/right plant by delta. If no plant is currently being displayed or delta is 0, pick the latest one.""" studies = self.history.get_studies() # if there are no plants to display, don't do anything if len(studies) == 0: return # if no plant is being displayed or 0 is provided, pick the last one if self.plant is None or delta == 0: index = -1 # if one is, find it and move by delta else: current_index = self.history.get_studies().index(self.plant_study) index = max(min(current_index + delta, len(studies) - 1), 0) # TODO: check for correct formatting, don't just crash if it's wrong self.plant = pickle.loads(studies[index]["plant"]) self.plant_study = studies[index] # TODO: check for correct formatting, don't just crash if it's wrong self.plant_date_label.setText(self.plant_study["date"].strftime("%-d/%-m/%Y")) self.plant_duration_label.setText(f"{int(self.plant_study['duration'])} minutes") self.canvas.set_drawable(self.plant) self.slider_value_changed() # it didn't, but the code should act as if it did (update plant) def save(self): """Save the current state of the plant to a file.""" if self.plant is not None: name, _ = QFileDialog.getSaveFileName(self, 'Save File', "", "SVG files (*.svg)") if name == "": return if not name.endswith(".svg"): name += ".svg" self.plant.save(name, 1000, 1000)
class IMUChartView(QChartView): aboutToClose = pyqtSignal(QObject) cursorMoved = pyqtSignal(datetime.datetime) def __init__(self, parent=None): super(QChartView, self).__init__(parent=parent) #self.setFixedHeight(400) #self.setMinimumHeight(500) """self.setMaximumHeight(700) self.setFixedHeight(700) self.setMinimumWidth(1500) self.setSizePolicy(QSizePolicy.Fixed,QSizePolicy.Fixed)""" self.reftime = datetime.datetime.now() self.cursor = QGraphicsLineItem() self.scene().addItem(self.cursor) self.decim_factor = 1 # self.setScene(QGraphicsScene()) self.chart = QChart() # self.scene().addItem(self.chart) self.setChart(self.chart) self.chart.legend().setVisible(True) self.chart.legend().setAlignment(Qt.AlignTop) self.ncurves = 0 self.setRenderHint(QPainter.Antialiasing) self.setRubberBand(QChartView.HorizontalRubberBand) # X, Y label on bottom # self.xTextItem = QGraphicsSimpleTextItem(self.chart) # self.xTextItem.setText('X: ') # self.yTextItem = QGraphicsSimpleTextItem(self.chart) # self.yTextItem.setText('Y: ') # self.update_x_y_coords() # Track mouse self.setMouseTracking(True) # Top Widgets newWidget = QWidget(self) newLayout = QHBoxLayout() newLayout.setContentsMargins(0, 0, 0, 0) newWidget.setLayout(newLayout) #labelx = QLabel(self) #labelx.setText('X:') #self.labelXValue = QLabel(self) #labely = QLabel(self) #labely.setText('Y:') #self.labelYValue = QLabel(self) # Test buttons #newLayout.addWidget(QToolButton(self)) #newLayout.addWidget(QToolButton(self)) #newLayout.addWidget(QToolButton(self)) # Spacer #newLayout.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Minimum)) # Labels """newLayout.addWidget(labelx) newLayout.addWidget(self.labelXValue) self.labelXValue.setMinimumWidth(200) self.labelXValue.setMaximumWidth(200) newLayout.addWidget(labely) newLayout.addWidget(self.labelYValue) self.labelYValue.setMinimumWidth(200) self.labelYValue.setMaximumWidth(200) """ """if parent is not None: parent.layout().setMenuBar(newWidget) """ # self.layout() self.build_style() def build_style(self): self.setStyleSheet("QLabel{color:blue;}") self.setBackgroundBrush(QBrush(Qt.darkGray)) self.chart.setPlotAreaBackgroundBrush(QBrush(Qt.black)) self.chart.setPlotAreaBackgroundVisible(True) def save_as_png(self, file_path): pixmap = self.grab() child = self.findChild(QOpenGLWidget) painter = QPainter(pixmap) if child is not None: d = child.mapToGlobal(QPoint()) - self.mapToGlobal(QPoint()) painter.setCompositionMode(QPainter.CompositionMode_SourceAtop) painter.drawImage(d, child.grabFramebuffer()) painter.end() pixmap.save(file_path, 'PNG') def closeEvent(self, QCloseEvent): self.aboutToClose.emit(self) @pyqtSlot(QPointF) def lineseries_clicked(self, point): print('lineseries clicked', point) @pyqtSlot(QPointF) def lineseries_hovered(self, point): print('lineseries hovered', point) def update_x_y_coords(self): pass # self.xTextItem.setPos(self.chart.size().width() / 2 - 100, self.chart.size().height() - 40) # self.yTextItem.setPos(self.chart.size().width() / 2 + 100, self.chart.size().height() - 40) def decimate(self, xdata, ydata): assert (len(xdata) == len(ydata)) # Decimate only if we have too much data decimate_factor = len(xdata) / 100000.0 if decimate_factor > 1.0: decimate_factor = int(np.floor(decimate_factor)) #print('decimate factor', decimate_factor) # x = decimate(xdata, decimate_factor) # y = decimate(ydata, decimate_factor) self.decim_factor = decimate_factor x = np.ndarray(int(len(xdata) / decimate_factor), dtype=np.float64) y = np.ndarray(int(len(ydata) / decimate_factor), dtype=np.float64) for i in range(len(x)): index = i * decimate_factor assert (index < len(xdata)) x[i] = xdata[index] y[i] = ydata[index] if x[i] < x[0]: print('timestamp error', x[i], x[0]) #print('return size', len(x), len(y), 'timestamp', x[0]) return x, y else: return xdata, ydata @pyqtSlot(float, float) def axis_range_changed(self, min, max): #print('axis_range_changed', min, max) for axis in self.chart.axes(): axis.applyNiceNumbers() def update_axes(self): # Get and remove all axes for axis in self.chart.axes(): self.chart.removeAxis(axis) # Create new axes # Create axis X # axisX = QDateTimeAxis() # axisX.setTickCount(5) # axisX.setFormat("dd MMM yyyy") # axisX.setTitleText("Date") # self.chart.addAxis(axisX, Qt.AlignBottom) # axisX.rangeChanged.connect(self.axis_range_changed) axisX = QValueAxis() axisX.setTickCount(10) axisX.setLabelFormat("%li") axisX.setTitleText("Seconds") self.chart.addAxis(axisX, Qt.AlignBottom) # axisX.rangeChanged.connect(self.axis_range_changed) # Create axis Y axisY = QValueAxis() axisY.setTickCount(5) axisY.setLabelFormat("%.3f") axisY.setTitleText("Values") self.chart.addAxis(axisY, Qt.AlignLeft) # axisY.rangeChanged.connect(self.axis_range_changed) ymin = None ymax = None # Attach axes to series, find min-max for series in self.chart.series(): series.attachAxis(axisX) series.attachAxis(axisY) vect = series.pointsVector() for i in range(len(vect)): if ymin is None: ymin = vect[i].y() ymax = vect[i].y() else: ymin = min(ymin, vect[i].y()) ymax = max(ymax, vect[i].y()) # Update range # print('min max', ymin, ymax) if ymin is not None: axisY.setRange(ymin, ymax) # Make the X,Y axis more readable axisX.applyNiceNumbers() # axisY.applyNiceNumbers() def add_data(self, xdata, ydata, color=None, legend_text=None): curve = QLineSeries() pen = curve.pen() if color is not None: pen.setColor(color) pen.setWidthF(1.5) curve.setPen(pen) #curve.setUseOpenGL(True) # Decimate xdecimated, ydecimated = self.decimate(xdata, ydata) # Data must be in ms since epoch # curve.append(self.series_to_polyline(xdecimated * 1000.0, ydecimated)) for i in range(len(xdecimated)): # TODO hack x = xdecimated[i] - xdecimated[0] curve.append(QPointF(x, ydecimated[i])) self.reftime = datetime.datetime.fromtimestamp(xdecimated[0]) if legend_text is not None: curve.setName(legend_text) # Needed for mouse events on series self.chart.setAcceptHoverEvents(True) # connect signals / slots # curve.clicked.connect(self.lineseries_clicked) # curve.hovered.connect(self.lineseries_hovered) # Add series self.chart.addSeries(curve) self.ncurves += 1 self.update_axes() def set_title(self, title): # print('Setting title: ', title) #self.chart.setTitle(title) pass def series_to_polyline(self, xdata, ydata): """Convert series data to QPolygon(F) polyline This code is derived from PythonQwt's function named `qwt.plot_curve.series_to_polyline`""" # print('series_to_polyline types:', type(xdata[0]), type(ydata[0])) size = len(xdata) polyline = QPolygonF(size) for i in range(0, len(xdata)): polyline[i] = QPointF(xdata[i] - xdata[0], ydata[i]) # pointer = polyline.data() # dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo # pointer.setsize(2*polyline.size()*tinfo(dtype).dtype.itemsize) # memory = np.frombuffer(pointer, dtype) # memory[:(size-1)*2+1:2] = xdata # memory[1:(size-1)*2+2:2] = ydata return polyline def add_test_data(self): # 100Hz, one day accelerometer values npoints = 1000 * 60 * 24 xdata = np.linspace(0., 10., npoints) self.add_data(xdata, np.sin(xdata), color=Qt.red, legend_text='Acc. X') # self.add_data(xdata, np.cos(xdata), color=Qt.green, legend_text='Acc. Y') # self.add_data(xdata, np.cos(2 * xdata), color=Qt.blue, legend_text='Acc. Z') self.set_title("Simple example with %d curves of %d points " \ "(OpenGL Accelerated Series)" \ % (self.ncurves, npoints)) def mouseMoveEvent(self, e: QMouseEvent): # Handling rubberbands super().mouseMoveEvent(e) # Go back to seconds (instead of ms) """xmap = self.chart.mapToValue(e.pos()).x() ymap = self.chart.mapToValue(e.pos()).y() self.labelXValue.setText(str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) self.labelYValue.setText(str(ymap))""" # self.xTextItem.setText('X: ' + str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) # self.yTextItem.setText('Y: ' + str(ymap)) def mousePressEvent(self, e: QMouseEvent): # Handling rubberbands super().mousePressEvent(e) self.setCursorPosition(e.pos().x(), True) pass def setCursorPosition(self, pos, emit_signal=False): # print (pos) pen = self.cursor.pen() pen.setColor(Qt.cyan) pen.setWidthF(1.0) self.cursor.setPen(pen) # On Top self.cursor.setZValue(100.0) area = self.chart.plotArea() x = pos y1 = area.y() y2 = area.y() + area.height() # self.cursor.set self.cursor.setLine(x, y1, x, y2) self.cursor.show() xmap = self.chart.mapToValue(QPointF(pos, 0)).x() ymap = self.chart.mapToValue(QPointF(pos, 0)).y() #self.labelXValue.setText(str(datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp()))) #self.labelYValue.setText(str(ymap)) if emit_signal: self.cursorMoved.emit( datetime.datetime.fromtimestamp(xmap + self.reftime.timestamp())) self.update() def setCursorPositionFromTime(self, timestamp, emit_signal=False): # Converts timestamp to x value pos = self.chart.mapToPosition( QPointF((timestamp - self.reftime).total_seconds(), 0)).x() self.setCursorPosition(pos, emit_signal) def mouseReleaseEvent(self, e: QMouseEvent): # Handling rubberbands super().mouseReleaseEvent(e) pass def resizeEvent(self, e: QResizeEvent): super().resizeEvent(e) # Update cursor height area = self.chart.plotArea() line = self.cursor.line() self.cursor.setLine(line.x1(), area.y(), line.x2(), area.y() + area.height()) # self.scene().setSceneRect(0, 0, e.size().width(), e.size().height()) # Need to reposition X,Y labels self.update_x_y_coords()
acmeSeries.append(line) categories.append( QDateTime.fromMSecsSinceEpoch(line.timestamp()).toString("dd")) # [2] # [3] create the chart chart = QChart() chart.addSeries(acmeSeries) chart.setTitle("Acme Ltd Historical Data (July 2015)") chart.setAnimationOptions(QChart.SeriesAnimations) # [3] # [4] set the axes properties chart.createDefaultAxes() axisX = chart.axes(Qt.Horizontal)[0] axisX.setCategories(categories) axisY = chart.axes(Qt.Vertical)[0] axisY.setMax(axisY.max() * 1.01) axisY.setMin(axisY.min() * 0.99) # [4] # [5] chart.legend().setVisible(True) chart.legend().setAlignment(Qt.AlignBottom) # [5] # [6] prepare the chart view chartView = QChartView(chart) chartView.setRenderHint(QPainter.Antialiasing)
class SproutUI(QtWidgets.QMainWindow): def __init__(self): super(SproutUI, self).__init__() uic.loadUi('Sprout.ui', self) self.setWindowTitle("Sprout") self.setWindowIcon(QIcon('./Images/SproutIcon.ico')) self.get_default_path() self.save_window_ui = SaveWindow(self) self.chartView = None self.myThread = None # columns: wedges, rows: rings (r1(w1,w2,...,w7,wAvg),r2(...),r3(...),rAvg(...)) self.densities = [] self.measurement_data = [] self.error_message = "" self.loading_image = QPixmap("./Images/LoadingImage") def ui(self): """ Sets Sprout user interface and displays the user interface :return: None """ self.tabWidget_1.setCurrentIndex(0) self.tabWidget_2.setCurrentIndex(0) self.lineEdit_numMeasurements.setFocus(0) # Main Screen and save button self.browse_button_1.clicked.connect(self.browse_file) self.browse_button_2.clicked.connect(self.browse_folder) self.lineEdit_intermediateStepPath.setText(in_data['intermediate_path']) self.lineEdit_numMeasurements.editingFinished.connect(self.update_num_diameter_measurements) self.lineEdit_numWedges.editingFinished.connect(self.update_num_wedges) self.lineEdit_numRings.editingFinished.connect(self.update_num_rings) self.lineEdit_imageDPI.editingFinished.connect(self.update_image_dpi) self.start_button.clicked.connect(self.start_button_func) self.stop_button.clicked.connect(self.stop_button_func) self.save_button.clicked.connect(self.show_save_files) # Graphs View self.comboBox_rings.currentIndexChanged.connect(self.filter_rings_graph) self.comboBox_wedges.currentIndexChanged.connect(self.filter_wedges_graph) self.progressBar.setValue(0) self.progressBar.hide() self.label_progressBar.hide() self.disable_dashboard() self.progressBar.valueChanged.connect(self.progress_change) self.tabWidget_1.tabBar().setCursor(QtCore.Qt.PointingHandCursor) self.tabWidget_2.tabBar().setCursor(QtCore.Qt.PointingHandCursor) self.show() def get_default_path(self): """ Gets the "Documents" directory of the user of the local computer. :return: None """ temp = str(os.path.expanduser("~")) split = temp.split(os.path.sep) temp = str(split[0] + "/" + split[1] + "/" + split[2] + "/" + "Documents/Sprout/Run") in_data['intermediate_path'] = temp def browse_file(self): """ Function mapped to the browse button used to select the file path of the image that will be used to calculate the fiber density of a bamboo cross-section. :return: None """ url = QFileDialog.getOpenFileName(self, "Open a file", "", "*jpg; *jpeg;; *bmp;; *tif") if url[0] is not '': try: self.label_bamboo.setPixmap(self.loading_image) self.label_bamboo.repaint() except Exception: self.warning_message_box("Missing loading image. ") try: assert (url[0])[-4:] in ('.bmp', '.jpg', 'jpeg', '.tif'), "Image format is not supported." except Exception as e: self.label_bamboo.clear() self.warning_message_box(str(e)) return try: cross_section = QPixmap(url[0]) cross_section = cross_section.scaled(500, 500) if cross_section.isNull(): self.label_bamboo.clear() self.warning_message_box("Unable to open input file. \n\n") else: self.label_bamboo.clear() self.label_bamboo.setPixmap(cross_section) self.lineEdit_imagePath.setText(url[0]) in_data['img_path'] = url[0] except Exception as e: self.label_bamboo.clear() self.warning_message_box("Unable to open input file, verify \n file path or image file type.\n\n") def browse_folder(self): """ Function mapped to the browse button used to select the folder path that will be used to save the intermediate step for the fiber density calculation. :return: None """ url = QFileDialog.getExistingDirectory(self, "Open a directory", "", QFileDialog.ShowDirsOnly) if url is not '': self.lineEdit_intermediateStepPath.setText(url) in_data['intermediate_path'] = url def disable_dashboard(self): """ Disables the dashboard and hides the graphs. :return: None """ self.dashboard_tab.setEnabled(False) self.tabWidget_2.setEnabled(False) self.graphs_tab.setEnabled(False) self.region_density_tab.setEnabled(False) self.measurement_data_tab.setEnabled(False) self.tabWidget_2.setCurrentIndex(0) self.widget_rings.hide() self.widget_wedges.hide() self.comboBox_rings.hide() self.comboBox_wedges.hide() def update_num_diameter_measurements(self): """ Update the value for the total number of measurements in the user interface main screen that serves as feedback. :return: None """ global in_data if self.is_int_inbound(self.lineEdit_numMeasurements.text(), 3, 100): temp = int(self.lineEdit_numMeasurements.text()) * 4 self.label_numMeasurementsFeedback.setText("Total Diameter Measurements: " + str(temp)) in_data['num_measurement'] = temp else: self.lineEdit_numMeasurements.clear() def update_num_wedges(self): """ Update the value for number of wedges in the user interface main screen that serves as feedback. :return: None """ global in_data, wedge_degree if self.is_int_inbound(self.lineEdit_numWedges.text(), 3, 100): temp = int(self.lineEdit_numWedges.text()) * 4 wedge_degree = 360 / temp self.label_numWedgesFeedback.setText("Num. Wedges: " + str(temp) + " @ {:.1f}º".format(wedge_degree)) self.label_numRegionsFeedback.setText("Num. Regions: " + str(temp * int(in_data['num_rings']))) in_data['num_wedges'] = temp else: self.lineEdit_numWedges.clear() def update_num_rings(self): """ Update the value for number of rings in the user interface main screen that serves as feedback. :return: None """ global in_data temp = self.lineEdit_numRings.text() if self.is_int_inbound(temp, 1, 25): self.label_numRingsFeedback.setText("Num. Rings: " + str(temp)) self.label_numRegionsFeedback.setText("Num. Regions: " + str(int(temp) * int(in_data['num_wedges']))) in_data['num_rings'] = int(temp) else: self.lineEdit_numRings.clear() def update_image_dpi(self): """ Update the value for the image dpi (dot per inch) in the user interface main screen that serves as feedback. :return: None """ global in_data if self.is_int_inbound(self.lineEdit_imageDPI.text(), 1200, 4800): temp = int(self.lineEdit_imageDPI.text()) self.label_imageDPIFeedback.setText("Image DPI: " + str(temp)) in_data['img_dpi'] = temp else: self.lineEdit_imageDPI.clear() def start_button_func(self): """ Sets what the start button will do when it is pressed. It starts the fiber density calculation and disables user input. The button is replaced by the stop button. :return: None """ global in_data, debounce # if program has not started if debounce is not 0: if (time.time() - debounce) < .30: return debounce = 0 # Test input data for being empty if(self.lineEdit_imagePath.text() is "" or self.lineEdit_intermediateStepPath.text() is "" or self.lineEdit_numWedges.text() is "" or self.lineEdit_numRings.text() is "" or self.lineEdit_numMeasurements.text() is "" or self.lineEdit_imageDPI.text() is ""): self.warning_message_box("Make sure all inputs are filled in.") return # Test numeric input if not self.is_int_inbound(self.lineEdit_numMeasurements.text(), 3, 100, self.label_numMeasurements.text()): return if not self.is_int_inbound(self.lineEdit_numWedges.text(), 3, 100, self.label_numWedges.text()): return if not self.is_int_inbound(self.lineEdit_numRings.text(), 1, 25, self.label_numRings.text()): return if not self.is_int_inbound(self.lineEdit_imageDPI.text(), 1200, 4800, self.label_imageDPI.text()): return # After all inputs have been validated self.disable_dashboard() # Save input data in in_data dictionary in_data['units'] = self.comboBox_units.currentText() in_data['num_measurement'] = int(self.lineEdit_numMeasurements.text())*4 in_data['img_dpi'] = int(self.lineEdit_imageDPI.text()) in_data['enhance'] = bool(self.checkBox_imageEnhancement.isChecked()) in_data['pixelMap'] = bool(self.checkBox_pixelMap.isChecked()) self.inputs_set_enabled(False) self.progressBar.show() self.label_progressBar.show() self.progressBar.setValue(1) self.stop_button.setEnabled(True) self.start_button.hide() # Start Sprout Controller for fiber density calculation self.myThread = Sprout.SproutController(self, in_data) try: self.myThread.start() self.myThread.progress.connect(self.progressBar.setValue) except: self.warning_message_box("Error while starting process.") def stop_button_func(self): """ Sets what the stop button will do when it is pressed or is called when the fiber density calculation is completed. It cancels the running session and enables user input. If running session is completed it enables the user input, and proceeds to display the dashboard (graphs, region density table, and measurement data). The button is replaced by the start button. :return: None """ global debounce # if program is currently in progress self.stop_button.setEnabled(False) self.stop_button.repaint() if not self.myThread.isFinished(): self.myThread.requestInterruption() self.myThread.wait() if self.progressBar.value() == 100: # if finished successfully # self.progressBar.setValue(100) self.dashboard_tab.setEnabled(True) self.tabWidget_2.setEnabled(True) self.graphs_tab.setEnabled(True) self.region_density_tab.setEnabled(True) self.measurement_data_tab.setEnabled(True) # create graphs self.create_graphs() # create table self.create_table() # set measurement data self.display_measurement_data() self.tabWidget_1.setCurrentIndex(1) self.tabWidget_2.setCurrentIndex(0) self.inputs_set_enabled(True) self.progressBar.hide() self.label_progressBar.hide() self.progressBar.setValue(0) self.start_button.show() debounce = time.time() def inputs_set_enabled(self, val: bool): """ Enable or disable the options presented in the home screen depending on input parameter: val. :param val: True to anabel all the options in the home screen and False to disable. :return: None """ self.browse_button_1.setEnabled(val) self.browse_button_2.setEnabled(val) self.comboBox_units.setEnabled(val) self.lineEdit_numMeasurements.setEnabled(val) self.lineEdit_numWedges.setEnabled(val) self.lineEdit_numRings.setEnabled(val) self.lineEdit_imageDPI.setEnabled(val) self.checkBox_imageEnhancement.setEnabled(val) self.checkBox_pixelMap.setEnabled(val) self.label_imagePath.setEnabled(val) self.label_intermediateStepPath.setEnabled(val) self.label_numMeasurements.setEnabled(val) self.label_numWedges.setEnabled(val) self.label_numRings.setEnabled(val) self.label_imageDPI.setEnabled(val) self.label_units.setEnabled(val) def create_graphs(self): """ Creates the graphs that will be displayed int the dashboard's Graphs tab. :return: None """ global default_comboBox_graph_item_count # Set Graphs ComboBox for x in range(self.comboBox_rings.count()): self.comboBox_rings.removeItem(default_comboBox_graph_item_count) for x in range(self.comboBox_wedges.count()): self.comboBox_wedges.removeItem(default_comboBox_graph_item_count) self.comboBox_rings.setCurrentIndex(0) self.comboBox_wedges.setCurrentIndex(0) # Ring Graph self.ring_chart = QChart() for x in range(len(self.densities)): ring_series = QLineSeries() for y in range(len(self.densities[x])-1): ring_series.append(y+1, self.densities[x][y]) self.ring_chart.addSeries(ring_series) if x < len(self.densities)-1: self.comboBox_rings.addItem("Ring " + str(x+1)) self.ring_chart.setTitle('Fiber Density VS Wedges') self.ring_chart.legend().hide() self.ring_chart.createDefaultAxes() self.ring_chart.axes(Qt.Horizontal)[0].setRange(1, len(self.densities[0])-1) self.ring_chart.axes(Qt.Vertical)[0].setRange(0, 1) self.ring_chart.axes(Qt.Horizontal)[0].setTitleText("Wedge Number") self.ring_chart.axes(Qt.Vertical)[0].setTitleText("Fiber Density") self.chartView = QChartView(self.ring_chart, self.widget_rings) self.chartView.resize(self.widget_rings.size()) # Wedges Graph self.wedge_chart = QChart() for y in range(len(self.densities[0])): if in_data['num_rings'] == 1: ring_series = QScatterSeries() else: ring_series = QLineSeries() for x in range(len(self.densities)-1): ring_series.append(x+1, self.densities[x][y]) self.wedge_chart.addSeries(ring_series) if y < len(self.densities[0])-1: self.comboBox_wedges.addItem("Wedge " + str(y+1)) self.wedge_chart.setTitle('Fiber Density VS Rings') self.wedge_chart.legend().hide() self.wedge_chart.createDefaultAxes() if (len(self.densities)) == 2: self.wedge_chart.axes(Qt.Horizontal)[0].setRange(0, 2) else: self.wedge_chart.axes(Qt.Horizontal)[0].setRange(1, len(self.densities)-1) self.wedge_chart.axes(Qt.Vertical)[0].setRange(0, 1) self.wedge_chart.axes(Qt.Horizontal)[0].setTitleText("Ring Number") self.wedge_chart.axes(Qt.Vertical)[0].setTitleText("Fiber Density") self.chartView = QChartView(self.wedge_chart, self.widget_wedges) self.chartView.resize(self.widget_wedges.size()) self.widget_rings.show() self.widget_wedges.show() if in_data['num_rings'] == 1: self.comboBox_rings.hide() else: self.comboBox_rings.show() self.comboBox_wedges.show() def filter_rings_graph(self): """ Filters the rings graph by: All(includes average), All Wedges(only rings are shown), Average(only average is shown), and individual rings(varies depending on number of rings). :return: None """ global default_comboBox_graph_item_count for x in range(len(self.ring_chart.series())): self.ring_chart.series()[x].show() if self.comboBox_rings.currentText() == "All Rings": for x in range(len(self.ring_chart.series()) - 1): self.ring_chart.series()[x].show() self.ring_chart.series()[len(self.ring_chart.series()) - 1].hide() elif self.comboBox_rings.currentText() == "Average": for x in range(len(self.ring_chart.series()) - 1): self.ring_chart.series()[x].hide() self.ring_chart.series()[len(self.ring_chart.series()) - 1].show() elif "Ring" in self.comboBox_rings.currentText(): for x in range(len(self.ring_chart.series())): self.ring_chart.series()[x].hide() self.ring_chart.series()[self.comboBox_rings.currentIndex() - default_comboBox_graph_item_count].show() def filter_wedges_graph(self): """ Filters the wedges graph by: All(includes average), All Wedges(only wedges are shown), Average(only average is shown), and individual wedges(varies depending on number of wedges). :return: None """ global default_comboBox_graph_item_count for x in range(len(self.wedge_chart.series())): self.wedge_chart.series()[x].show() if self.comboBox_wedges.currentText() == "All Wedges": for x in range(len(self.wedge_chart.series()) - 1): self.wedge_chart.series()[x].show() self.wedge_chart.series()[len(self.wedge_chart.series()) - 1].hide() elif self.comboBox_wedges.currentText() == "Average": for x in range(len(self.wedge_chart.series()) - 1): self.wedge_chart.series()[x].hide() self.wedge_chart.series()[len(self.wedge_chart.series()) - 1].show() elif "Wedge" in self.comboBox_wedges.currentText(): for x in range(len(self.wedge_chart.series())): self.wedge_chart.series()[x].hide() self.wedge_chart.series()[self.comboBox_wedges.currentIndex() - default_comboBox_graph_item_count].show() def create_table(self): """ Creates the table that will be presented in the dashboard's Region Density tab based on fiber density calculations. :return: None """ i = 0 j = 0 column_name = [] row_name = [] self.tableWidget.setRowCount(len(self.densities)) self.tableWidget.setColumnCount(len(self.densities[0])) for ring in self.densities: for wedge in ring: item = QTableWidgetItem("{:.4f}".format(wedge)) item.setTextAlignment(Qt.AlignCenter) # if average of average if j == (len(self.densities[0])-1) and i == (len(self.densities)-1): font = QFont() font.setBold(True) item.setFont(font) self.tableWidget.setItem(i, j, item) j += 1 j = 0 i += 1 for x in range(len(self.densities[0])): if x == len(self.densities[0]) - 1: column_name.append("Average") else: column_name.append("Wedge " + str(x + 1)) for y in range(len(self.densities)): if y == len(self.densities) - 1: row_name.append("Average") else: row_name.append("Ring " + str(y + 1)) self.tableWidget.setHorizontalHeaderLabels(column_name) self.tableWidget.setVerticalHeaderLabels(row_name) self.tableWidget.setEditTriggers(QtWidgets.QTableWidget.NoEditTriggers) def display_measurement_data(self): """ Manages all output data related to the measurement data that is presented in the dashboard Measurement Data. :return: None """ self.lineEdit_area.setText(str("{:.4f}".format(self.measurement_data[0])) + " " + in_data['units'] + "^2") self.lineEdit_avgOuterDiameter.setText(str("{:.4f}".format(self.measurement_data[1])) + " " + in_data['units']) self.lineEdit_avgInnerDiameter.setText(str("{:.4f}".format(self.measurement_data[2])) + " " + in_data['units']) self.lineEdit_AverageT.setText(str("{:.4f}".format(self.measurement_data[3])) + " " + in_data['units']) self.lineEdit_centroid_x.setText(str("{:.4f}".format(self.measurement_data[4])) + " " + in_data['units']) self.lineEdit_centroid_y.setText(str("{:.4f}".format(self.measurement_data[5])) + " " + in_data['units']) self.lineEdit_momentOfInertia_x.setText(str("{:.4f}".format(self.measurement_data[6])) + " " + in_data['units'] + "^4") self.lineEdit_momentOfInertia_y.setText(str("{:.4f}".format(self.measurement_data[7])) + " " + in_data['units'] + "^4") self.lineEdit_productOfInertia.setText(str("{:.4f}".format(self.measurement_data[8])) + " " + (in_data['units']) + "^4") def is_int_inbound(self, ui_in: str, lower: int, upper: int, ui_in_name: str = None): """ Test if user input is in specified upper and lower bound, including upper and lower bound value. :param ui_in: user input for value that will be tested :param lower: lowest value that ui_in can have to return True :param upper: highest value that ui_in can hava to return True :param ui_in_name: user input label name that is used in popup messages for users to relate error :return: True if ui_in is in upper and lower bound (lower <= ui_in <= upper) otherwise False """ if not (str.isdigit(ui_in)) or int(ui_in) > upper or int(ui_in) < lower: if ui_in_name is not None: self.warning_message_box(str(ui_in_name) + "\nPlease input a number from " + str(lower) + " to " + str(upper)) return False else: return True def warning_message_box(self, message): """ Display a popup message box to inform users of error. :param message: Message to be displayed in the popup message :return: """ mbox = QMessageBox.critical(self, "Warning!!", message) if mbox == QMessageBox.Ok: self.lineEdit_numMeasurements.setFocus(0) def show_save_files(self): """ Call to displays the popup for the Save Popup Window. :return: None """ self.windowModality() self.save_window_ui.lineEdit_filePath.setText(os.getcwd()) self.save_window_ui.show() self.save_window_ui.raise_() def progress_change(self): """ Signals the user interface when process is completed or wen error occurs. :return: None """ if self.progressBar.value() == 2: self.stop_button_func() self.warning_message_box(str(self.error_message)) elif self.progressBar.value() == 100: self.densities = DM.get_fiber_density_average() self.measurement_data = DM.get_dimensional_measurements() self.stop_button_func()
class MyWidget(QWidget): def __init__(self): super().__init__() uic.loadUi('ws_ui.ui', self) self.con = sqlite3.connect("ws_database.db") self.setWindowFlags(QtCore.Qt.FramelessWindowHint) self.create_linechart() self.tmp, self.hmd, self.prs = 0, 0, 0 self.make_measure() self.measure_timer = QTimer(self) self.measure_timer.setInterval(MEASURE_FREQUENCIES * 1000) self.measure_timer.timeout.connect(self.make_measure) self.measure_timer.start() self.update_timer = QTimer(self) self.update_timer.setInterval(1000) self.update_timer.timeout.connect(self.update_labels) self.update_timer.start() self.update_labels() def quit(self): self.destroy() quit() def make_measure(self): sns = sensor_measure() if sns[0] != ERROR_CODE: self.tmp = sns[0] else: print('tmp error') if sns[1] != ERROR_CODE: self.hmd = sns[1] else: print('hmd error') if sns[2] != ERROR_CODE: self.prs = sns[2] else: print('prs error') time = int(dt.datetime.now().timestamp()) req = """ INSERT INTO short_term_data(tmp, hmd, prs, time_from_epoch) VALUES(?,?,?,?) """ self.con.execute(req, (self.tmp, self.hmd, self.prs, time)) self.con.commit() self.update_linechart() def update_labels(self): deg = u'\N{DEGREE SIGN}' hpa = 'ʰᴾᵃ' self.time_label.setText(dt.datetime.now().strftime('%H:%M')) self.tmp_label.setText('{} {}C'.format(self.tmp, deg)) self.hmd_label.setText('{} %'.format(self.hmd)) self.prs_label.setText('{} {}'.format(self.prs, hpa)) def create_linechart(self): self.chart = QChart() self.chart.legend().hide() self.series = QLineSeries() self.axisValue = QValueAxis() self.axisCurrentTime = QValueAxis() self.axisTime = QDateTimeAxis() self.axisTime.setFormat("hh:mm") self.chartview = QChartView(self.chart, self.groupBox) self.chartview.resize(540, 460) self.chartview.move(0, 0) self.chartview.setRenderHint(QPainter.Antialiasing) def update_linechart(self): if self.axisTime in self.chart.axes(): self.chart.removeAxis(self.axisTime) if self.axisCurrentTime in self.chart.axes(): self.chart.removeAxis(self.axisCurrentTime) if self.axisValue in self.chart.axes(): self.chart.removeAxis(self.axisValue) if self.series in self.chart.series(): self.chart.removeSeries(self.series) self.series.clear() self.axisValue.setMax(50) self.axisValue.setMin(-50) req = """ SELECT tmp, time_from_epoch FROM short_term_data WHERE (time_from_epoch - ?) < 86400 AND NOT tmp = ? """ cur = self.con.cursor() result = list(cur.execute(req, (int(dt.datetime.now().timestamp()), ERROR_CODE))) for measure in result: self.series.append(measure[1] * 1000, measure[0]) self.axisTime.setMin(QDateTime.fromMSecsSinceEpoch(int(dt.datetime.now().timestamp()) * 1000 - 86390000)) self.axisTime.setMax(QDateTime.fromMSecsSinceEpoch(int(dt.datetime.now().timestamp()) * 1000)) self.chart.addSeries(self.series) self.chart.addAxis(self.axisTime, Qt.AlignBottom) self.series.attachAxis(self.axisTime) self.chart.addAxis(self.axisValue, Qt.AlignLeft) self.series.attachAxis(self.axisValue) self.chart.setTitle('Температура')