Added 'save as image' feature

This commit is contained in:
ayxan 2022-11-25 19:30:48 +04:00
parent 8863cbb070
commit 31dca7953c
5 changed files with 111 additions and 25 deletions

View File

@ -8,24 +8,36 @@ if str(main_path) not in path:
import plotly.express as px import plotly.express as px
import streamlit as st import streamlit as st
from data_access import LocalDataAccess from data_access import (LocalDataAccess,
UploadDataAccess)
st.set_page_config(page_title = 'Health Data Visualization', st.set_page_config(page_title = 'Health Data Visualization',
page_icon = ':bar_chart:', page_icon = ':bar_chart:',
layout = 'wide') layout = 'wide')
local_data = LocalDataAccess('data') upload_data = st.file_uploader('Choose a Huawei Health data',
_ = local_data.get_data('huawei_health_data.json') type=['json'])
heart_rate = local_data.get_heart_rate()
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(f'All Data ({len(heart_rate)})', False):
if st.sidebar.checkbox('Average of Days', 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'} labels = {'x': 'Date of The Day', 'y': 'Average Heart Rate'}
else: 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'} labels = {'x': 'Date and Time', 'y': 'Heart Rate'}
else: else:
@ -36,7 +48,7 @@ else:
max_value = heart_rate[-1]['time'] max_value = heart_rate[-1]['time']
).strftime("%d-%m-%Y") ).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'} labels = {'x': 'Date and Time', 'y': 'Heart Rate'}
st.sidebar.header('Split Data:') st.sidebar.header('Split Data:')
@ -48,7 +60,7 @@ average_number = st.sidebar.number_input(
) )
if average_number > 1: 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'} labels = {'x': 'Number of Heart Rate', 'y': 'Heart Rate'}
chart_type = st.sidebar.selectbox( chart_type = st.sidebar.selectbox(
@ -56,6 +68,12 @@ chart_type = st.sidebar.selectbox(
('Line', 'Scatter', 'Bar') ('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( st.plotly_chart(
{'Line': px.line, 'Scatter': px.scatter, 'Bar': px.bar}[chart_type]( {'Line': px.line, 'Scatter': px.scatter, 'Bar': px.bar}[chart_type](
x = x, y = y, x = x, y = y,

View File

@ -1 +1,2 @@
from .local_data_access import LocalDataAccess from .local_data_access import LocalDataAccess
from .upload_data_access import UploadDataAccess

View File

@ -1,4 +1,8 @@
import pandas as pd 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, from datetime import (datetime,
timedelta) timedelta)
from typing import (List, from typing import (List,
@ -6,13 +10,22 @@ from typing import (List,
Union) Union)
class DataOperations: class DataOperations(ABC):
heart_rate = None _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_wrapper(func):
def __is_data_none(self, *args, **kwargs) -> Union[None, NameError]: def __is_data_none(self, *args, **kwargs) -> Union[None, NameError]:
if self.data is None: 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 func(self, *args, **kwargs)
return __is_data_none return __is_data_none
@ -23,13 +36,14 @@ class DataOperations:
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
return __is_heart_rate_none return __is_heart_rate_none
@property
@is_data_none_wrapper @is_data_none_wrapper
def get_heart_rate(self) -> List: def heart_rate(self) -> List:
# sourcery skip: inline-immediately-returned-variable, list-comprehension # sourcery skip: inline-immediately-returned-variable, list-comprehension
self.heart_rate = [] self._heart_rate = []
for d in self.data: for d in self.data:
if d['type'] == 7: if d['type'] == 7:
self.heart_rate.append( self._heart_rate.append(
{ {
'rate': float(d['samplePoints'][0]['value']), 'rate': float(d['samplePoints'][0]['value']),
'time': (datetime.fromtimestamp( 'time': (datetime.fromtimestamp(
@ -40,11 +54,11 @@ class DataOperations:
) )
} }
) )
return self.heart_rate return self._heart_rate
@is_heart_rate_none_wrapper @is_heart_rate_none_wrapper
def get_heart_rate_for_all_days_as_axis(self) -> Tuple: 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 ( return (
list ( list (
map( map(
@ -60,7 +74,7 @@ class DataOperations:
heart_rate_grouped = pd.DataFrame( heart_rate_grouped = pd.DataFrame(
list( list(
map( 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'] ).groupby('date', sort=False).mean()['rate']
@ -72,7 +86,7 @@ class DataOperations:
heart_rate = pd.DataFrame( heart_rate = pd.DataFrame(
list( list(
filter( 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
) )
) )
) )
@ -96,3 +110,36 @@ class DataOperations:
heart_rate2.append(sum(t) / len(t)) heart_rate2.append(sum(t) / len(t))
return (range(len(heart_rate2)), heart_rate2) 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)

View File

@ -1,16 +1,19 @@
from pathlib import Path from pathlib import Path
import json
from typing import List from typing import List
import json
from .data_opeartions import DataOperations from .data_opeartions import DataOperations
class LocalDataAccess(DataOperations): class LocalDataAccess(DataOperations):
def __init__(self, data_folder_name: str) -> None: def __init__(self, data_folder_name: str) -> None:
self.data = None
self.data_dir: Path =\ self.data_dir: Path =\
Path(__file__).resolve().parent.parent.parent / data_folder_name 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: with open(self.data_dir / file_name) as f:
self.data: List = json.load(f) self._data = json.load(f)
return self.data

View File

@ -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)