-
Notifications
You must be signed in to change notification settings - Fork 0
/
display.py
231 lines (193 loc) · 7.61 KB
/
display.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
"""
JSON to Line Chart Visualization Script
This script visualizes special JSON data generated by the 'generate.py' script using line charts.
The data consists of blood-test results or similar tests in a well-known Turkish 'Probel' hospital system
Requirements:
- matplotlib: comprehensive library for creating static, animated, and interactive visualizations in Python
Usage:
1. Run the 'generate.py' with already downloaded PDF files in 'pdf' directory and have the 'data.json' file.
2. Run the script to visualize the data in the 'data.json' file.
3. Use the combobox or arrow buttons to navigate between different data.
Author: Eray Ozturk | erayozturk1@gmail.com
URL: github.com/diffstorm
Date: 01/10/2023
"""
import os
import sys
import re
import json
import tkinter as tk
from tkinter import ttk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from datetime import datetime
from collections import Counter
# Database file that all the parsed data recorded
data_file = "data.json"
# Create a variable to keep track of the previous chart
previous_chart = None
figure, ax = None, None
current_index = 0 # Keep track of the current selected index
def read_json(file_name):
"""
Read data from a JSON file and return it as a dictionary.
If the file doesn't exist, return an empty dictionary.
"""
if os.path.exists(file_name):
try:
with open(file_name, 'r', encoding='utf-8') as json_file:
data = json.load(json_file)
return data
except (json.JSONDecodeError, FileNotFoundError):
return {}
else:
return {}
def list_all_names(data):
"""
Extract all unique names from the data and return them in alphabetical order.
"""
names = sorted(set(entry['name'] for entry in data))
return names
def list_all_numbers_in_text(input_text):
"""
Extract all positive numbers from a given text.
This function returns a list of positive numbers found in the input_text.
"""
numbers = re.findall(r'-?\d+\.\d+|-?\d+', input_text)
numbers = [abs(float(num)) for num in numbers]
return numbers
def extract_lower_upper(range_str):
"""
Extract lower and upper limit values from a range string.
This function returns lower and upper values as a tuple.
"""
upper = 0.0
lower = 0.0
numbers = list_all_numbers_in_text(range_str.strip())
n = len(numbers)
if n == 1:
upper = numbers[0]
elif n >= 2:
numbers = numbers[:2]
numbers.sort()
upper, lower = numbers[:2]
return min(lower, upper), max(lower, upper)
def most_common_string(strings):
"""
Find the most common string in a list of strings.
"""
string_counts = Counter(strings)
most_common, count = string_counts.most_common(1)[0]
return most_common
def draw_line_chart(data, selected_name):
"""
Draw a line chart for the selected data entry.
"""
global previous_chart, figure, ax, current_index
# Filter the data for the selected name
selected_data = [entry for entry in data if entry['name'] == selected_name]
# Sort the data by date in ascending order using datetime
selected_data.sort(key=lambda x: datetime.strptime(x['date'], '%d/%m/%Y %H:%M'))
# Extract dates, values, units, and ranges from the selected data
dates = [entry['date'] for entry in selected_data]
values = [float(entry['value'].replace(',', '.')) for entry in selected_data]
units = [entry['unit'] for entry in selected_data if entry['unit']]
tranges = [entry['range'] for entry in selected_data if entry['range']]
common_unit = most_common_string(units)
common_range = most_common_string(tranges)
lower, upper = extract_lower_upper(common_range)
# Clear the previous chart before drawing the new one
if previous_chart:
previous_chart.get_tk_widget().pack_forget()
plt.close(figure)
figure, ax = plt.subplots(figsize=(10, 6))
ax.plot(dates, values, marker='o', linestyle='-', color='b')
ax.set_xlabel('Date')
ax.set_ylabel('Value')
ax.set_title(f'{selected_name} | Unit: {common_unit} | Range: {common_range} | {len(dates)} records')
plt.xticks(rotation=90)
ax.grid(True)
# Plot upper and lower limit lines if available
ax.axhline(y=upper, color='r', linestyle='--', label='Upper Limit')
ax.axhline(y=lower, color='g', linestyle='--', label='Lower Limit')
# Add values of upper and lower limits on top of the lines
ax.annotate(f'Upper: {upper:.2f}', xy=(dates[-1], upper), xytext=(5, 0), textcoords='offset points', color='r')
ax.annotate(f'Lower: {lower:.2f}', xy=(dates[-1], lower), xytext=(5, 0), textcoords='offset points', color='g')
# Add values on every data point
for date, value in zip(dates, values):
is_in_range = lower <= value <= upper
text_weight = 'normal' if is_in_range else 'bold'
ax.annotate(f'{value:.2f}', (date, value), textcoords="offset points", xytext=(0,10), ha='center', fontsize=8, weight=text_weight)
ax.legend()
plt.tight_layout()
# Update the previous chart reference with the new chart
previous_chart = FigureCanvasTkAgg(figure, root)
previous_chart.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
# Reset the current index to the newly selected name
current_index = names.index(selected_name)
def on_combobox_selected(event):
"""
Callback function when a name is selected from the combobox.
"""
selected_name = combobox.get()
draw_line_chart(data, selected_name)
def next_name():
"""
Display the next name in the combobox.
"""
global current_index
if current_index < len(names) - 1:
current_index += 1
selected_name = names[current_index]
combobox.set(selected_name)
draw_line_chart(data, selected_name)
def previous_name():
"""
Display the previous name in the combobox.
"""
global current_index
if current_index > 0:
current_index -= 1
selected_name = names[current_index]
combobox.set(selected_name)
draw_line_chart(data, selected_name)
def on_exit():
"""
Callback function when the application is closed.
"""
global root
root.quit()
if __name__ == "__main__":
# Read data from the JSON file
data = read_json(data_file)
if not data:
print(f"ERROR: File '{data_file}' does not exist or is empty or is invalid.")
print("Please run generate.py first or try deleting the file if the error persists.")
exit(1)
# Create the main window
root = tk.Tk()
root.title("JSON to Line Chart")
root.geometry("1024x768")
# Create the custom combobox frame
combobox_frame = ttk.Frame(root)
combobox_frame.pack(pady=10)
# Create the previous button
previous_button = ttk.Button(combobox_frame, text="<<", command=previous_name)
previous_button.grid(row=0, column=0)
# Create and populate the combo box with all names
names = list_all_names(data)
combobox = ttk.Combobox(combobox_frame, values=names, state="readonly", width=45)
combobox.bind("<<ComboboxSelected>>", on_combobox_selected)
combobox.grid(row=0, column=1)
# Create the next button
next_button = ttk.Button(combobox_frame, text=">>", command=next_name)
next_button.grid(row=0, column=2)
# Draw the initial chart with the first name in the list
if names:
selected_name = names[current_index]
combobox.set(selected_name)
draw_line_chart(data, selected_name)
# Bind the exit event
root.protocol("WM_DELETE_WINDOW", on_exit)
# Start the GUI main loop
root.mainloop()