Преглед на файлове

separate graph helper methods and add tests

Daniel Sheffield преди 1 година
родител
ревизия
a050a708e1
променени са 5 файла, в които са добавени 118 реда и са изтрити 48 реда
  1. 9 23
      app/activities/PriceCheck.py
  2. 7 23
      app/activities/TransactionEditor.py
  3. 1 2
      app/activities/grouped_widget_util.py
  4. 34 0
      app/data/dataframe_util.py
  5. 67 0
      test/data/test_dataframe_util.py

+ 9 - 23
app/activities/PriceCheck.py

@@ -19,6 +19,7 @@ from urwid import (
     Text,
 )
 
+from ..data.dataframe_util import get_caption, get_time_range, stats
 from ..widgets import (
     AutoCompleteEdit,
     AutoCompleteFloatEdit,
@@ -118,31 +119,15 @@ class PriceCheck(FocusWidget):
         ).truncate(
             before=max(0, len(df.index)-self.graph._canvas_width)
         )
-        data = df[['$/unit','quantity']].apply(
-            lambda x: (float(x['$/unit']), float(x['quantity'])),
+        data = df[['price', '$/unit','quantity']].apply(
+            lambda x: (float(x['price']), float(x['$/unit']), float(x['quantity'])),
             axis=1, result_type='broadcast'
         )
-        data['avg'] = (data['$/unit']*data['quantity']).sum()/data['quantity'].sum()
-        data_max = data.max()['$/unit'] #.max()
-        assert len(data['avg'].unique()) == 1
         norm = [ (x,) for x in data['$/unit'] ] #.to_records(index=False)
-        self.graph.set_data(norm, data_max,
-            vscale=list(map(float, [
-                data['$/unit'].min(),
-                data['$/unit'].median(),
-                data['avg'].iloc[0],
-                data_max
-            ]))
-        )
-        #self.graph.set_bar_width(1)
-        # canvas_width = 10 + pad + pad + 10
-        date_strlen = (self.graph.canvas_width - 20)
-        ex = "─" if date_strlen % 2 else ""
-        plen = date_strlen//2
-        caption = f"{df['ts_raw'].min():%d/%m/%Y}"
-        caption += f"{{p:>{plen}}}{ex}{{p:<{plen}}}".format(
-            p="─")
-        caption += f"{df['ts_raw'].max():%d/%m/%Y}"
+        scale = stats(data, 'price', 'quantity', '$/unit')
+        self.graph.set_data(norm, scale[-1], vscale=scale)
+        time_range = get_time_range(df, 'ts_raw')
+        caption = get_caption(time_range, self.graph.canvas_width)
         self.graph.set_caption(caption)
 
     def update_historic_prices(self, data):
@@ -177,10 +162,11 @@ class PriceCheck(FocusWidget):
         ]
         self.rating.update_rating(_avg, _min, _max, unit, price=price, quantity=quantity)
 
+        self.update_graph(df)
         self.text_fields['dbview'].set_text(
             get_historic_prices(df)
         )
-        self.update_graph(df)
+
 
     def __init__(self,
         activity_manager: ActivityManager,

+ 7 - 23
app/activities/TransactionEditor.py

@@ -7,7 +7,6 @@
 from decimal import Decimal, InvalidOperation
 from itertools import chain
 from typing import (
-    Callable,
     Union,
     Tuple,
     List,
@@ -29,6 +28,7 @@ from urwid import (
     Text,
 )
 
+from ..data.dataframe_util import get_caption, get_time_range, stats
 from ..data.QueryManager import QueryManager
 from .grouped_widget_util import (
     to_numbered_field,
@@ -226,31 +226,15 @@ class TransactionEditor(FocusWidget):
         ).truncate(
             before=max(0, len(df.index)-self.graph._canvas_width)
         )
-        data = df[['$/unit','quantity']].apply(
-            lambda x: (float(x['$/unit']), float(x['quantity'])),
+        data = df[['price', '$/unit','quantity']].apply(
+            lambda x: (float(x['price']), float(x['$/unit']), float(x['quantity'])),
             axis=1, result_type='broadcast'
         )
-        data['avg'] = (data['$/unit']*data['quantity']).sum()/data['quantity'].sum()
-        data_max = data.max()['$/unit'] #.max()
-        assert len(data['avg'].unique()) == 1
         norm = [ (x,) for x in data['$/unit'] ] #.to_records(index=False)
-        self.graph.set_data(norm, data_max,
-            vscale=list(map(float, [
-                data['$/unit'].min(),
-                data['$/unit'].median(),
-                data['avg'].iloc[0],
-                data_max
-            ]))
-        )
-        #self.graph.set_bar_width(1)
-        # canvas_width = 10 + pad + pad + 10
-        date_strlen = (self.graph.canvas_width - 20)
-        ex = "─" if date_strlen % 2 else ""
-        plen = date_strlen//2
-        caption = f"{df['ts_raw'].min():%d/%m/%Y}"
-        caption += f"{{p:>{plen}}}{ex}{{p:<{plen}}}".format(
-            p="─")
-        caption += f"{df['ts_raw'].max():%d/%m/%Y}"
+        scale = stats(data, 'price', 'quantity', '$/unit')
+        self.graph.set_data(norm, scale[-1], vscale=scale)
+        time_range = get_time_range(df, 'ts_raw')
+        caption = get_caption(time_range, self.graph.canvas_width)
         self.graph.set_caption(caption)
 
     def update_historic_prices(self, data):

+ 1 - 2
app/activities/grouped_widget_util.py

@@ -6,7 +6,6 @@
 # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
 from typing import Callable, Tuple
 
-
 def to_numbered_field(x: Tuple[str, str]) -> Tuple[str, int, str]:
     if len(x[0].split('#', 1)) > 1:
         name, idx = x[0].split('#', 1)
@@ -27,4 +26,4 @@ def in_same_row(name: str) -> Callable[[Tuple[str, int, str]], bool]:
     return lambda x: x[1] == int(row)
 
 def to_named_value(name: str) -> Callable[[Tuple[int, str]], Tuple[str, str]]:
-    return lambda e: (f'{name}#{e[0]}', e[1])
+    return lambda e: (f'{name}#{e[0]}', e[1])

+ 34 - 0
app/data/dataframe_util.py

@@ -0,0 +1,34 @@
+#
+# Copyright (c) Daniel Sheffield 2023
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
+from typing import List, Tuple
+from pandas import DataFrame
+
+def stats(data: DataFrame, num: str, den: str, frac: str) -> List[float]:
+    return list(map(float, [
+        data[frac].min(),
+        data[frac].median(),
+        data[num].sum()/data[den].sum(), # mean
+        data[frac].max()
+    ]))
+
+def get_time_range(data: DataFrame, ts_col: str) -> Tuple[str, str]:
+    return f"{data[ts_col].min():%d/%m/%Y}" , f"{data[ts_col].max():%d/%m/%Y}"
+
+def get_divider(width: int, marker="─"):
+    if width <= 3:
+        return " ─ "
+
+    plen = width//2 - 1
+    ex = marker if (width % 2) and plen > 0 else ""
+    return f" {{p:>{plen}}}{ex}{{p:<{plen}}} ".format(p=marker)
+
+def get_caption(time_range: Tuple[str, str], width: int):
+    left, right = time_range
+    pad = width - len(left) - len(right)
+    return ''.join([
+        left, get_divider(pad, marker="─"), right
+    ])

+ 67 - 0
test/data/test_dataframe_util.py

@@ -0,0 +1,67 @@
+#
+# Copyright (c) Daniel Sheffield 2023
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
+from datetime import datetime
+from pytest import mark, raises
+from pandas import DataFrame
+
+from app.data.dataframe_util import(
+    get_caption,
+    get_divider,
+    get_time_range,
+    stats,
+)
+
+@mark.parametrize('data', [
+    DataFrame([
+        {'ts': datetime(1970, 1, 1)}, {'ts': datetime(2000, 2, 28)}
+    ])
+])
+def test_get_timerange(data):
+    assert ("01/01/1970", "28/02/2000") == get_time_range(data, 'ts')
+
+@mark.parametrize('width, marker, expected', [
+    #(0, '─', (AssertionError, "")),
+    #(1, '─', (AssertionError, "")),
+    #(2, '─', (AssertionError, "")),
+    (0, '─', ' ─ '),
+    (1, '─', ' ─ '),
+    (2, '─', ' ─ '),
+    (3, '─', ' ─ '),
+    (4, '─', ' ── '),
+    (5, '─', ' ─── '),
+    (6, '─', '  ──  '),
+    (7, '─', '  ───  '),
+    (8, '─', '   ──   '),
+])
+def test_get_divider(width, marker, expected):
+    if not isinstance(expected, tuple):
+        assert expected == get_divider(width, marker=marker)
+        return
+
+    with raises(expected[0]):
+        get_divider(width, marker=marker)
+
+@mark.parametrize('data', [
+    DataFrame([
+        {'ts': 0, 'price': 3.0, 'amount': 10, '$/unit': 0.3 },
+        {'ts': datetime(2000, 2, 28), 'price': 11, 'amount': 2, '$/unit': 5.5 },
+    ])
+])
+def test_stats(data):
+    assert [0.3, 2.9, 14/12, 5.5] == stats(data, 'price', 'amount', '$/unit')
+
+@mark.parametrize('data', [
+    DataFrame([
+        {'ts': datetime(1970, 1, 1)}, {'ts': datetime(2000, 2, 28)}
+    ])
+])
+@mark.parametrize('width, div', [
+    (26, ' ── '),
+    (27, ' ─── '),
+])
+def test_get_caption(data, width, div):
+    assert f"01/01/1970 {div} 28/02/2000" == get_caption(get_time_range(data, 'ts'), width)