diff --git a/src/app/main.py b/src/app/main.py index 9228e64..7244bba 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -8,24 +8,36 @@ if str(main_path) not in path: import plotly.express as px import streamlit as st -from data_access import LocalDataAccess +from data_access import (LocalDataAccess, + UploadDataAccess) st.set_page_config(page_title = 'Health Data Visualization', page_icon = ':bar_chart:', layout = 'wide') -local_data = LocalDataAccess('data') -_ = local_data.get_data('huawei_health_data.json') -heart_rate = local_data.get_heart_rate() +upload_data = st.file_uploader('Choose a Huawei Health data', + type=['json']) + +if upload_data is None: + st.sidebar.caption('Local test data is being used') + data_access = LocalDataAccess('data') + data_access.data = 'huawei_health_data.json' + +else: + st.sidebar.caption('Uploaded data is being used') + data_access = UploadDataAccess() + data_access.data = upload_data + +heart_rate = data_access.heart_rate if st.sidebar.checkbox(f'All Data ({len(heart_rate)})', False): if st.sidebar.checkbox('Average of Days', False): - x, y = local_data.get_average_heart_rate_for_days_as_axis() + x, y = data_access.get_average_heart_rate_for_days_as_axis() labels = {'x': 'Date of The Day', 'y': 'Average Heart Rate'} else: - x, y = local_data.get_heart_rate_for_all_days_as_axis() + x, y = data_access.get_heart_rate_for_all_days_as_axis() labels = {'x': 'Date and Time', 'y': 'Heart Rate'} else: @@ -36,7 +48,7 @@ else: max_value = heart_rate[-1]['time'] ).strftime("%d-%m-%Y") - x, y = local_data.get_heart_rate_for_one_day(day) + x, y = data_access.get_heart_rate_for_one_day(day) labels = {'x': 'Date and Time', 'y': 'Heart Rate'} st.sidebar.header('Split Data:') @@ -48,7 +60,7 @@ average_number = st.sidebar.number_input( ) if average_number > 1: - x, y = local_data.get_averages_of_heart_rates(y, average_number) + x, y = data_access.get_averages_of_heart_rates(y, average_number) labels = {'x': 'Number of Heart Rate', 'y': 'Heart Rate'} chart_type = st.sidebar.selectbox( @@ -56,6 +68,12 @@ chart_type = st.sidebar.selectbox( ('Line', 'Scatter', 'Bar') ) +st.sidebar.header('Save as image') +st.sidebar.caption('white pixels is min rate, black pixels is max rate, green is empty pixels') +st.sidebar.download_button('Download', + data=data_access.get_heart_rate_as_img(y), + file_name='heart-rate.png') + st.plotly_chart( {'Line': px.line, 'Scatter': px.scatter, 'Bar': px.bar}[chart_type]( x = x, y = y, diff --git a/src/data_access/__init__.py b/src/data_access/__init__.py index 4105972..c12161c 100644 --- a/src/data_access/__init__.py +++ b/src/data_access/__init__.py @@ -1 +1,2 @@ -from .local_data_access import LocalDataAccess \ No newline at end of file +from .local_data_access import LocalDataAccess +from .upload_data_access import UploadDataAccess \ No newline at end of file diff --git a/src/data_access/data_opeartions.py b/src/data_access/data_opeartions.py index 45168b6..94dfff9 100644 --- a/src/data_access/data_opeartions.py +++ b/src/data_access/data_opeartions.py @@ -1,4 +1,8 @@ import pandas as pd +from PIL import Image +from io import BytesIO +import numpy as np +from abc import ABC, abstractmethod from datetime import (datetime, timedelta) from typing import (List, @@ -6,13 +10,22 @@ from typing import (List, Union) -class DataOperations: - heart_rate = None +class DataOperations(ABC): + _heart_rate: Union[List, None] = None + _data: Union[List, None] = None + + @property + @abstractmethod + def data(self): ... + + @data.setter + @abstractmethod + def data(self): ... def is_data_none_wrapper(func): def __is_data_none(self, *args, **kwargs) -> Union[None, NameError]: if self.data is None: - raise NameError(f'data not found. You must call the get_data function before calling the {func.__name__} function') + raise NameError(f'data not found. You must set the data before calling the {func.__name__} function') return func(self, *args, **kwargs) return __is_data_none @@ -23,13 +36,14 @@ class DataOperations: return func(self, *args, **kwargs) return __is_heart_rate_none + @property @is_data_none_wrapper - def get_heart_rate(self) -> List: + def heart_rate(self) -> List: # sourcery skip: inline-immediately-returned-variable, list-comprehension - self.heart_rate = [] + self._heart_rate = [] for d in self.data: if d['type'] == 7: - self.heart_rate.append( + self._heart_rate.append( { 'rate': float(d['samplePoints'][0]['value']), 'time': (datetime.fromtimestamp( @@ -40,11 +54,11 @@ class DataOperations: ) } ) - return self.heart_rate + return self._heart_rate @is_heart_rate_none_wrapper def get_heart_rate_for_all_days_as_axis(self) -> Tuple: - heart_rate = pd.DataFrame(self.heart_rate) + heart_rate = pd.DataFrame(self._heart_rate) return ( list ( map( @@ -60,7 +74,7 @@ class DataOperations: heart_rate_grouped = pd.DataFrame( list( map( - lambda t: {'rate': t['rate'], 'date': t['time'].strftime("%d-%m-%Y")}, self.heart_rate + lambda t: {'rate': t['rate'], 'date': t['time'].strftime("%d-%m-%Y")}, self._heart_rate ) ) ).groupby('date', sort=False).mean()['rate'] @@ -72,7 +86,7 @@ class DataOperations: heart_rate = pd.DataFrame( list( filter( - lambda t: t['time'].strftime("%d-%m-%Y") == day, self.heart_rate + lambda t: t['time'].strftime("%d-%m-%Y") == day, self._heart_rate ) ) ) @@ -95,4 +109,37 @@ class DataOperations: t = rates[i:i + average_number] heart_rate2.append(sum(t) / len(t)) - return (range(len(heart_rate2)), heart_rate2) \ No newline at end of file + return (range(len(heart_rate2)), heart_rate2) + + @classmethod + def __array_to_bytes_image(cls, img_array: np.array) -> bytes: + buf = BytesIO() + Image.fromarray(img_array, 'RGB').save(buf, format="PNG") + return buf.getvalue() + + @staticmethod + def get_heart_rate_as_img(rates: List) -> bytes: + rates_len = len(rates) + old_max = max(rates) + old_min = min(rates) + new_max = 255 + new_min = 0 + + old_range = (old_max - old_min) + new_range = (new_max - new_min) + + for max_n in range(rates_len): + if max_n * max_n > rates_len: break + + img_array = np.full((max_n, max_n, 3), [0, 255, 0], dtype=np.uint8) + + x, y = 0, 0 + for n in rates: + color = (255 - (((n - old_min) * new_range) / old_range) + new_min) + img_array[x][y] = [color, color, color] + if y < max_n-1: y += 1 + else: + x += 1 + y = 0 + + return DataOperations.__array_to_bytes_image(img_array) \ No newline at end of file diff --git a/src/data_access/local_data_access.py b/src/data_access/local_data_access.py index 0e99b9b..b43e44c 100644 --- a/src/data_access/local_data_access.py +++ b/src/data_access/local_data_access.py @@ -1,16 +1,19 @@ from pathlib import Path -import json from typing import List +import json from .data_opeartions import DataOperations class LocalDataAccess(DataOperations): def __init__(self, data_folder_name: str) -> None: - self.data = None self.data_dir: Path =\ Path(__file__).resolve().parent.parent.parent / data_folder_name - def get_data(self, file_name: str) -> List: + @property + def data(self) -> List: + return self._data + + @data.setter + def data(self, file_name: str): with open(self.data_dir / file_name) as f: - self.data: List = json.load(f) - return self.data \ No newline at end of file + self._data = json.load(f) \ No newline at end of file diff --git a/src/data_access/upload_data_access.py b/src/data_access/upload_data_access.py new file mode 100644 index 0000000..3a2c089 --- /dev/null +++ b/src/data_access/upload_data_access.py @@ -0,0 +1,17 @@ +import json +from typing import List +from streamlit.runtime.uploaded_file_manager import UploadedFile +from .data_opeartions import DataOperations + + +class UploadDataAccess(DataOperations): + def __init__(self) -> None: + super().__init__() + + @property + def data(self) -> List: + return self._data + + @data.setter + def data(self, uploaded_file: UploadedFile) -> None: + self._data = json.load(uploaded_file) \ No newline at end of file