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
def init_chart(self): print("Initialazing") self.de_chart.setMaximumDate(QDate.currentDate()) self.de_chart.setDateTime(QDateTime.currentDateTime()) limits = { "calcium": 1000, "iron": 8, "magnesium": 400, "potassium": 4700, "sodium": 1500, "phosphorus": 700, "zinc": 11, "vitaminc": 90, "vitk": 120, "vite": 15, "vitaminb6": 1, "vitb12": 2, "thiamin": 1 } self.idx_kcal = [ ("Macronutrients", "Macronutrients from {} to {}", #Dictionary where key is what we want to chart from foodnutrition and #value is description to show in legend { "carb": "Oglj. hidrati", "lipid": "Maščobe", "protein": "Beljakovine", "sugar": "Sladkor", "fiber": "Vlaknine", "fasat": "Nasičene maščobe", }, lambda nutrient, x: x, QBarSeries, "%.2f g" ), ("Micronutrients", "Micronutrients from {} to {}", { "calcium": "Kalcij", "iron": "Železo", "magnesium": "Magnezij", "potassium": "Kalij", "sodium": "Natrij", "phosphorus": "Fosfor", "zinc": "Zinc" }, lambda nutrient, x: x, QBarSeries, "%.2f mg" ), ("Micronutrients in % RDA", "Micronutrients in % from RDA from {} to {}", { "calcium": "Kalcij", "iron": "Železo", "magnesium": "Magnezij", "potassium": "Kalij", "sodium": "Natrij", "phosphorus": "Fosfor", "zinc": "Zinc" }, lambda nutrient, x: x/limits[nutrient]*100, QBarSeries, "%d %%" ), ("Vitamins", "Vitamins from {} to {}", { "vitaminc": "Vitamin C", "vitaminb6": "Vitamin B6", "vitb12": "Vitamin B12", "thiamin": "Thiamin", "vite": "Vitamin E", "vitk": "Vitamin K" }, lambda nutrient, x: x, QBarSeries, "%.2f mg" ), ("Vitamins in % RDA", "Vitamins in % from RDA from {} to {}", { "vitaminc": "Vitamin C", "vitaminb6": "Vitamin B6", "vitb12": "Vitamin B12", "thiamin": "Thiamin", "vite": "Vitamin E", "vitk": "Vitamin K" }, lambda nutrient, x: x/limits[nutrient]*100, QBarSeries, "%.2f %%" ), ("Macronutrients kcal", "Macronutrients in kcal from {} to {}", { "protein": "Beljakovine", "lipid": "Maščobe", "carb": "Oglj. hidrati", }, lambda nutrient, value: value*4 if nutrient=="carb" or \ nutrient=="protein" else value*9, QStackedBarSeries, "%.2f kcal" ), ("Macronutrients kcal %", "Macronutrients in % of kcal from {} to {}", { "protein": "Beljakovine", "lipid": "Maščobe", "carb": "Oglj. hidrati", }, lambda nutrient, value: value*4 if nutrient=="carb" or \ nutrient=="protein" else value*9, QPercentBarSeries, "%d %" ), ("Macronutrients line", "Macronutrients from {} to {}", #Dictionary where key is what we want to chart from foodnutrition and #value is description to show in legend { "carb": "Oglj. hidrati", "lipid": "Maščobe", "protein": "Beljakovine", "sugar": "Sladkor", "fiber": "Vlaknine", "fasat": "Nasičene maščobe", }, lambda nutrient, x: x, QLineSeries, "%.2f g" ), ] self.cb_kcal_chart_type.addItems([x[0] for x in self.idx_kcal]) series = [] series2 = [] axisX = None axisX2 = None protein_y = None kcal_y = None water_y = None def default_values(today): """ Calculates default values for kcal and protein KCAL: Based on 2200 kcal. 1300 should be consumed before lunch. Protein is similar """ from config import (needed_protein_2_hours_till_lunch, needed_kcal_2_hours_till_lunch, lunch_kcal, lunch_protein, needed_kcal_2_hours_after_lunch, needed_protein_2_hours_after_lunch) ds = datetime(today.year, today.month, today.day, 7) now = datetime.today() print(now) qt_now = to_qdate(now) print(qt_now) default_series = QLineSeries() default_series.setName("default kcal") protein_series = QLineSeries() protein_series.setName("default protein") #default_series.setUseOpenGL(True) water_series = QLineSeries() water_series.setName("default voda") sum_kcal = 0 sum_protein = 0 sum_water = 0 for date in rrule(HOURLY, ds, 2, count=8): qdate = to_qdate(date) #print (date, qdate, date.timestamp()*1000, qdate.toMSecsSinceEpoch()) date_msec = int(date.timestamp() * 1000) default_series.append(date_msec, sum_kcal) protein_series.append(date_msec, sum_protein) water_series.append(date_msec, sum_water) #print (sum_kcal) if date.hour < 15: sum_kcal += needed_kcal_2_hours_till_lunch sum_protein += needed_protein_2_hours_till_lunch elif date.hour == 15: sum_kcal += lunch_kcal sum_protein += lunch_protein else: sum_kcal += needed_kcal_2_hours_after_lunch sum_protein += needed_protein_2_hours_after_lunch sum_water += 300 #print (sum_kcal) return default_series, protein_series, water_series def chart_date_changed(date): """ Updates chart with selected date It also needs to update axis x and set range on it """ #print(date) today = date.toPyDateTime().date() #today = datetime(today.year, today.month, today.day) tomorow = (today + dateutil.relativedelta.relativedelta(days=1)) print(today, "-", tomorow) self.init_kcal_week_chart(date, self.cb_kcal_chart_type.currentIndex()) # Joinedload loads stuff in one query instead of multiple ones #def query(start, end): ##func.sum(FoodNutrition.kcal).label("kcal_sum"), ##func.sum(FoodNutrition.protein).label("protein_sum")) \ #items = self.session.query( #FoodNutrition.kcal, FoodNutrition.protein) \ #.join(Item) \ #.filter(Item.time.between(start, end)) ##.filter(Item.calc_nutrition != None) \ ##.group_by(True) ##.order_by(Item.time) ##.filter(FoodNutrition.id==Item.calc_nutrition) \ #return items #today_date = today.date() #ds =datetime(today.date().year,today.date().month,today.date().day,9) #for date in rrule(HOURLY, ds, 2, count=8): #items = query(today, date) #for item in items: #print(item) items = self.session.query(Item) \ .options(joinedload(Item.nutri_info)) \ .filter(Item.calc_nutrition != None) \ .filter(Item.time.between(today, tomorow)) \ .order_by(Item.time) kcal_series = QLineSeries() kcal_series.setName("kcal") protein_series = QLineSeries() protein_series.setName("protein") water_series = QLineSeries() water_series.setName("water") sum_kcal = 0 sum_protein = 0 sum_water = 0 # Calculates sum of kcal and protein for item in items: #print (item.description, item.nutri_info.kcal, item.nutri_info.protein) sum_kcal += item.nutri_info.kcal sum_protein += item.nutri_info.protein sum_water += item.nutri_info.water kcal_series.append(int(item.time.timestamp() * 1000), sum_kcal) protein_series.append((item.time.timestamp() * 1000), sum_protein) water_series.append((item.time.timestamp() * 1000), sum_water) #Adds current date time as last item in series to see on graph where we #are now kcal_series.append(QDateTime.currentDateTime().toMSecsSinceEpoch(), sum_kcal) protein_series.append(QDateTime.currentDateTime().toMSecsSinceEpoch(), sum_protein) water_series.append(QDateTime.currentDateTime().toMSecsSinceEpoch(), sum_water) #print (sum_kcal, sum_protein) kcal_y.setMax(max(needed_kcal, sum_kcal)) protein_y.setMax(max(needed_protein, sum_protein)) water_y.setMax(max(2100, sum_water)) #print ("BEFORE CHANGE:") #print (axisX.min(), axisX.max()) #print (datetime.fromtimestamp(default_kcal_s.at(0).x()/1000)) #Updates X axis so current datetime is used ds = datetime(today.year, today.month, today.day, 7) for idx, date in enumerate(rrule(HOURLY, ds, 2, count=8)): point = default_kcal_s.at(idx) default_kcal_s.replace(idx, int(date.timestamp() * 1000), point.y()) default_protein_s.replace(idx, int(date.timestamp() * 1000), default_protein_s.at(idx).y()) default_water_s.replace(idx, int(date.timestamp() * 1000), default_water_s.at(idx).y()) #print ("AFTER") #Currently x axis is from current date (input of this function) + 15 hours axisX.setRange(to_qdate(ds), to_qdate(ds, 15)) axisX2.setRange(to_qdate(ds), to_qdate(ds, 15)) #print (axisX.min(), axisX.max()) #print (datetime.fromtimestamp(default_kcal_s.at(0).x()/1000)) #print (point.x(), point.y()) #print (idx, date, datetime.fromtimestamp(point.x()/1000), point.y()) chart = self.chartView_bar.chart() for serie in series: chart.removeSeries(serie) series.clear() series.append(kcal_series) series.append(protein_series) chart.addSeries(kcal_series) kcal_series.attachAxis(axisX) kcal_series.attachAxis(kcal_y) chart.addSeries(protein_series) protein_series.attachAxis(axisX) protein_series.attachAxis(protein_y) chart2 = self.chartView_water.chart() for serie in series2: chart2.removeSeries(serie) series2.clear() series2.append(water_series) chart2.addSeries(water_series) water_series.attachAxis(axisX2) water_series.attachAxis(water_y) #print(kcal_series) default_kcal_s, default_protein_s, default_water_s = default_values( datetime.today().date()) #self.chartView_bar.chart().removeAllSeries() self.chartView_bar.setRenderHint(QPainter.Antialiasing) chart = self.chartView_bar.chart() #Sets X Axis axisX = QDateTimeAxis() axisX.setFormat("h:mm") self.chartView_bar.chart().addAxis(axisX, Qt.AlignBottom) self.chartView_bar.chart().addSeries(default_kcal_s) #Sets kcal axis kcal_y = QValueAxis() kcal_y.setLinePenColor(default_kcal_s.pen().color()) kcal_y.setLabelFormat("%.2f kcal") chart.addAxis(kcal_y, Qt.AlignLeft) default_kcal_s.attachAxis(axisX) default_kcal_s.attachAxis(kcal_y) chart.addSeries(default_protein_s) #Sets protein axis protein_y = QValueAxis() protein_y.setLinePenColor(default_protein_s.pen().color()) protein_y.setLabelFormat("%.2f g") #protein_y.setGridLinePen((default_protein_s.pen())) chart.addAxis(protein_y, Qt.AlignRight) default_protein_s.attachAxis(axisX) default_protein_s.attachAxis(protein_y) chart2 = self.chartView_water.chart() self.chartView_water.setRenderHint(QPainter.Antialiasing) #Sets X Axis axisX2 = QDateTimeAxis() axisX2.setFormat("h:mm") chart2.addSeries(default_water_s) water_y = QValueAxis() water_y.setLinePenColor(default_water_s.pen().color()) water_y.setLabelFormat("%.2f g") chart2.addAxis(water_y, Qt.AlignLeft) chart2.addAxis(axisX2, Qt.AlignBottom) default_water_s.attachAxis(axisX2) default_water_s.attachAxis(water_y) def init_kcal_week_chart(today, chart_idx_type): today_o = today.toPyDateTime().date() today = (today_o + dateutil.relativedelta.relativedelta(days=1)) last_week = (today + dateutil.relativedelta.relativedelta(days=-7)) print(last_week, "-", today) chart_data = self.idx_kcal[chart_idx_type] self.chartView_kcal.chart() \ .setTitle(chart_data[1].format(last_week.strftime("%a %d. %b"), today_o.strftime("%a %d. %b"))) keys = chart_data[2] sets = {} entities = [sa.func.strftime("%Y-%m-%d", Item.time).label("day")] is_barchart = True if isinstance(chart_data[4](), QLineSeries): is_barchart = False #print ("BARCHART:", is_barchart, chart_data[4], QLineSeries) #Creates barsets where each barset is one value in foodnutrition #Each value in barset is different day #Also creates sqlalchemy sum select func_multiplay = chart_data[3] for nutrition, nutrition_name in keys.items(): if is_barchart: sets[nutrition] = QBarSet(nutrition_name) else: sets[nutrition] = chart_data[4]() sets[nutrition].setName(nutrition_name) entities.append( sa.func.sum(func_multiplay(nutrition,getattr(FoodNutrition, nutrition))) \ .label(nutrition)) #Query which select all foodnutrition between last_week and today and sums it #up grouped by day items = \ sa.orm.query.Query(entities, session=self.session) \ .join(FoodNutrition) \ .filter(Item.time.between(last_week, today)) \ .group_by("day") \ .order_by(Item.time) #print (items) #print (items.column_descriptions) #items.column_descriptions #je list z dictinariy kjer je key name pove imena columnov for item in items: #print ("ITEM:", item) if not is_barchart: tm = item.day dt = dateutil.parser.parse(tm) en = int(dt.timestamp() * 1000) for key, bar_set in sets.items(): if key in item.keys(): if is_barchart: bar_set.append(getattr(item, key)) else: #print (bar_set.name(), en, getattr(item, key)) bar_set.append(en, getattr(item, key)) else: if is_barchart: bar_set.append(0) else: bar_set.append(en, 0) #print ("SETS:", sets) if is_barchart: bar_series = chart_data[4]() for value in sets.values(): bar_series.append(value) #Creates x axis with dates string_dates = [] for date in rrule(DAILY, last_week, count=7): string_dates.append(date.strftime("%a %d. %b")) axis = QBarCategoryAxis() axis.append(string_dates) else: axis = QDateTimeAxis() axis.setFormat("d. M. yyyy") #Sets date ticks as many as we want to show dates axis.setTickCount(list(sets.values())[0].count()) axis_y = QValueAxis() axis_y.setLabelFormat(chart_data[5]) axis_y.setTickCount(10) chart = self.chartView_kcal.chart() chart.setAnimationOptions(QChart.SeriesAnimations) chart.removeAllSeries() for cur_axis in chart.axes(): chart.removeAxis(cur_axis) if is_barchart: chart.addSeries(bar_series) else: self.chartView_kcal.setRenderHint(QPainter.Antialiasing) chart.addAxis(axis, Qt.AlignBottom) chart.addAxis(axis_y, Qt.AlignLeft) for series in sets.values(): #print ("SERIES:", series.name()) chart.addSeries(series) series.attachAxis(axis) series.attachAxis(axis_y) axis_y.setRange(0, 150) #chart.setAxisX(axis, series) #chart.setAxisY(axis_y, series) if is_barchart: chart.createDefaultAxes() chart.setAxisX(axis, bar_series) chart.axisY(bar_series).setLabelFormat(chart_data[5]) chart.axisY(bar_series).setTickCount(10) chart.legend().setVisible(True) chart.legend().setAlignment(Qt.AlignBottom) def show_daily_chart(): today = self.de_chart.dateTime().toPyDateTime().date() tomorow = today + dateutil.relativedelta.relativedelta( hour=23, minute=59, second=0) title, chart_title, data, func, *rest = self.idx_kcal[ self.cb_kcal_chart_type.currentIndex()] print("Daily chart:", today, tomorow, title) item_datas = [] items = self.session.query(Item) \ .options(joinedload(Item.nutri_info)) \ .filter(Item.calc_nutrition != None) \ .filter(Item.time.between(today, tomorow)) \ .order_by(Item.time) keys = list(data.keys()) for item in items: item_data = [item.time, item.description[:20]] for key in keys: #print ("KEY:", key) item_data.append(func(key, getattr(item.nutri_info, key))) item_datas.append(item_data) header = ["time", "desc"] + keys #for time, desc, *rest in item_datas: #print (time, desc,) #for t in rest: #print("{:.2f}".format(t)) cd = ChartDialog(self) cd.set_day_chart(header, today, chart_title, item_datas) cd.show() self.chart_date_changed = chart_date_changed self.init_kcal_week_chart = init_kcal_week_chart self.de_chart.dateTimeChanged.connect(self.chart_date_changed) self.chart_date_changed(self.de_chart.dateTime()) self.cb_kcal_chart_type.currentIndexChanged.connect( lambda idx: self.init_kcal_week_chart(self.de_chart.dateTime(), idx)) self.pb_show_day_chart.clicked.connect(show_daily_chart)