Improve look of graphs
This commit is contained in:
@@ -235,12 +235,45 @@ def generate_weekly_calendar(readings_by_day, selected_date, local_tz):
|
||||
]
|
||||
|
||||
def prepare_graph_data(readings):
|
||||
"""Prepare data for graph rendering."""
|
||||
"""Prepare data for graph rendering, reversing so chronological order is left-to-right."""
|
||||
chronological_readings = list(reversed(readings))
|
||||
|
||||
n = len(chronological_readings)
|
||||
time_percentages = []
|
||||
|
||||
systolic_vals = [r.systolic for r in chronological_readings]
|
||||
diastolic_vals = [r.diastolic for r in chronological_readings]
|
||||
hr_vals = [r.heart_rate for r in chronological_readings]
|
||||
|
||||
sys_avg = round(sum(systolic_vals) / n, 1) if n > 0 else 0
|
||||
dia_avg = round(sum(diastolic_vals) / n, 1) if n > 0 else 0
|
||||
hr_avg = round(sum(hr_vals) / n, 1) if n > 0 else 0
|
||||
|
||||
if n == 0:
|
||||
pass
|
||||
elif n == 1:
|
||||
time_percentages = [0.5]
|
||||
else:
|
||||
first_time = chronological_readings[0].timestamp.timestamp()
|
||||
last_time = chronological_readings[-1].timestamp.timestamp()
|
||||
time_span = last_time - first_time
|
||||
|
||||
for r in chronological_readings:
|
||||
if time_span == 0:
|
||||
time_percentages.append(0.5)
|
||||
else:
|
||||
p = (r.timestamp.timestamp() - first_time) / time_span
|
||||
time_percentages.append(p)
|
||||
|
||||
return {
|
||||
'timestamps': [r.timestamp.strftime('%b %d') for r in readings],
|
||||
'systolic': [r.systolic for r in readings],
|
||||
'diastolic': [r.diastolic for r in readings],
|
||||
'heart_rate': [r.heart_rate for r in readings],
|
||||
'timestamps': [r.timestamp.strftime('%b %d\n%H:%M') for r in chronological_readings],
|
||||
'systolic': systolic_vals,
|
||||
'diastolic': diastolic_vals,
|
||||
'heart_rate': hr_vals,
|
||||
'time_percentages': time_percentages,
|
||||
'sys_avg': sys_avg,
|
||||
'dia_avg': dia_avg,
|
||||
'hr_avg': hr_avg,
|
||||
}
|
||||
|
||||
def calculate_progress_badges(user_id, user_tz):
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -65,14 +65,14 @@
|
||||
<!-- Start Date -->
|
||||
<div>
|
||||
<label for="start_date" class="block text-sm font-medium text-gray-700">Start Date</label>
|
||||
<input type="date" name="start_date" id="start_date"
|
||||
<input type="date" name="start_date" id="start_date" value="{{ start_date or '' }}"
|
||||
class="w-full p-3 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||
</div>
|
||||
|
||||
<!-- End Date -->
|
||||
<div>
|
||||
<label for="end_date" class="block text-sm font-medium text-gray-700">End Date</label>
|
||||
<input type="date" name="end_date" id="end_date"
|
||||
<input type="date" name="end_date" id="end_date" value="{{ end_date or '' }}"
|
||||
class="w-full p-3 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500">
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,64 +1,327 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Blood Pressure Graph -->
|
||||
<style>
|
||||
.svg-tooltip-group .svg-tooltip-content {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.svg-tooltip-group:hover .svg-tooltip-content {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
<div class="space-y-8">
|
||||
<!-- Blood Pressure Graph Card -->
|
||||
<div class="bg-white rounded-2xl shadow-lg border border-gray-100 p-6 transition-all hover:shadow-xl">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-600 mb-2">Blood Pressure (mmHg)</h3>
|
||||
<svg viewBox="0 0 600 250" xmlns="http://www.w3.org/2000/svg" class="w-full">
|
||||
<!-- Axes -->
|
||||
<line x1="50" y1="200" x2="550" y2="200" stroke="black" stroke-width="1" /> <!-- X-axis -->
|
||||
<line x1="50" y1="20" x2="50" y2="200" stroke="black" stroke-width="1" /> <!-- Y-axis -->
|
||||
<h3 class="text-xl font-bold text-gray-800">Blood Pressure Trends</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">Systolic vs Diastolic over time (mmHg)</p>
|
||||
</div>
|
||||
<div class="flex space-x-6 text-sm mt-4 md:mt-0 bg-gray-50 px-4 py-2 rounded-full">
|
||||
<div class="flex items-center"><span
|
||||
class="w-3 h-3 rounded-full bg-blue-500 mr-2 shadow-sm"></span><span
|
||||
class="font-medium text-gray-700">Systolic</span></div>
|
||||
<div class="flex items-center"><span
|
||||
class="w-3 h-3 rounded-full bg-pink-500 mr-2 shadow-sm"></span><span
|
||||
class="font-medium text-gray-700">Diastolic</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Y-axis Labels (Blood Pressure Values) -->
|
||||
<div class="w-full overflow-x-auto pb-4">
|
||||
<svg viewBox="0 0 800 350" class="w-full min-w-[600px] h-auto drop-shadow-sm font-sans"
|
||||
style="max-height: 350px;">
|
||||
<defs>
|
||||
<linearGradient id="sysGrad" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#3b82f6" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#3b82f6" stop-opacity="0.0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="diaGrad" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#ec4899" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#ec4899" stop-opacity="0.0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{% if timestamps %}
|
||||
{% set n = timestamps|length %}
|
||||
{% set spacing = 700 / (n - 1) if n > 1 else 0 %}
|
||||
|
||||
<!-- Horizontal Grid Lines & Y-Axis Labels -->
|
||||
{% for value in range(50, 201, 50) %}
|
||||
<text x="40" y="{{ 200 - (value / 200 * 180) }}" font-size="10" text-anchor="end">{{ value }}</text>
|
||||
{% set y = 280 - (value / 200 * 250) %}
|
||||
<line x1="50" y1="{{ y }}" x2="750" y2="{{ y }}" stroke="#e5e7eb" stroke-width="1"
|
||||
stroke-dasharray="4 4" />
|
||||
<text x="40" y="{{ y + 4 }}" font-size="12" fill="#9ca3af" text-anchor="end" font-weight="500">{{ value
|
||||
}}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- X-axis Labels (Timestamps) -->
|
||||
{% for i in range(timestamps|length) %}
|
||||
<text x="{{ 50 + i * 50 }}" y="215" font-size="10" text-anchor="middle">{{ timestamps[i] }}</text>
|
||||
<!-- X-Axis Base Line -->
|
||||
<line x1="50" y1="280" x2="750" y2="280" stroke="#d1d5db" stroke-width="1" />
|
||||
|
||||
<!-- X-Axis Labels -->
|
||||
{% set ns = namespace(last_x=-100) %}
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
<!-- Optimization: only draw label if it has enough horizontal space from the last drawn label -->
|
||||
{% set is_last = (i == n - 1) %}
|
||||
{% if x - ns.last_x > 45 or is_last %}
|
||||
<!-- If it's the last label but too close to the previous, maybe we draw it anyway but it will overlap. We prevent overlap by aggressively spacing. -->
|
||||
{% if not is_last or x - ns.last_x > 40 or ns.last_x == -100 %}
|
||||
<text x="{{ x }}" y="310" font-size="11" fill="#6b7280" text-anchor="middle" font-weight="500"
|
||||
transform="rotate(-25 {{ x }} 310)">{{
|
||||
timestamps[i] }}</text>
|
||||
{% set ns.last_x = x %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Graph Lines -->
|
||||
<!-- Systolic Filled Area -->
|
||||
<polygon fill="url(#sysGrad)" points="{% if n>1 %}50{% else %}400{% endif %},280
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (systolic[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}
|
||||
{% if n>1 %}{{ 50 + (n - 1) * spacing }}{% else %}400{% endif %},280" />
|
||||
|
||||
<!-- Systolic Line -->
|
||||
<polyline fill="none" stroke="blue" stroke-width="2"
|
||||
points="{% for i in range(timestamps|length) %}{{ 50 + i * 50 }},{{ 200 - (systolic[i] / 200 * 180) }} {% endfor %}" />
|
||||
<polyline fill="none" stroke="#3b82f6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
|
||||
points="{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (systolic[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}" />
|
||||
|
||||
<!-- Systolic Points -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (systolic[i] / 200 * 250) %}
|
||||
<circle cx="{{ x }}" cy="{{ y }}" r="5" fill="#ffffff" stroke="#3b82f6" stroke-width="2.5"></circle>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Diastolic Filled Area -->
|
||||
<polygon fill="url(#diaGrad)" points="{% if n>1 %}50{% else %}400{% endif %},280
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (diastolic[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}
|
||||
{% if n>1 %}{{ 50 + (n - 1) * spacing }}{% else %}400{% endif %},280" />
|
||||
|
||||
<!-- Diastolic Line -->
|
||||
<polyline fill="none" stroke="red" stroke-width="2"
|
||||
points="{% for i in range(timestamps|length) %}{{ 50 + i * 50 }},{{ 200 - (diastolic[i] / 200 * 180) }} {% endfor %}" />
|
||||
<polyline fill="none" stroke="#ec4899" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
|
||||
points="{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (diastolic[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}" />
|
||||
|
||||
<!-- Axis Labels -->
|
||||
<text x="25" y="110" font-size="12" transform="rotate(-90, 25, 110)" text-anchor="middle">Blood Pressure
|
||||
(mmHg)</text>
|
||||
<text x="300" y="240" font-size="12" text-anchor="middle">Date</text>
|
||||
<!-- Diastolic Points -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (diastolic[i] / 200 * 250) %}
|
||||
<circle cx="{{ x }}" cy="{{ y }}" r="5" fill="#ffffff" stroke="#ec4899" stroke-width="2.5"></circle>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Tooltips (Rendered last to stay on top) -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
|
||||
{% set ySys = 280 - (systolic[i] / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: pointer;">
|
||||
<circle cx="{{ x }}" cy="{{ ySys }}" r="15" fill="transparent" />
|
||||
<circle cx="{{ x }}" cy="{{ ySys }}" r="7" fill="#ffffff" stroke="#3b82f6" stroke-width="2.5"
|
||||
class="svg-tooltip-content" />
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="{{ x - 64 }}" y="{{ ySys - 54 }}" width="128" height="40" fill="#1f2937" rx="6" />
|
||||
<polygon points="{{ x - 6 }},{{ ySys - 14 }} {{ x + 6 }},{{ ySys - 14 }} {{ x }},{{ ySys - 6 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="{{ x }}" y="{{ ySys - 36 }}" font-size="11" fill="#f3f4f6" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">{{ timestamps[i] }}</text>
|
||||
<text x="{{ x }}" y="{{ ySys - 22 }}" font-size="11" fill="#93c5fd" text-anchor="middle"
|
||||
font-weight="500" class="font-sans">Systolic: {{ systolic[i] }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{% set yDia = 280 - (diastolic[i] / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: pointer;">
|
||||
<circle cx="{{ x }}" cy="{{ yDia }}" r="15" fill="transparent" />
|
||||
<circle cx="{{ x }}" cy="{{ yDia }}" r="7" fill="#ffffff" stroke="#ec4899" stroke-width="2.5"
|
||||
class="svg-tooltip-content" />
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="{{ x - 64 }}" y="{{ yDia - 54 }}" width="128" height="40" fill="#1f2937" rx="6" />
|
||||
<polygon points="{{ x - 6 }},{{ yDia - 14 }} {{ x + 6 }},{{ yDia - 14 }} {{ x }},{{ yDia - 6 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="{{ x }}" y="{{ yDia - 36 }}" font-size="11" fill="#f3f4f6" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">{{ timestamps[i] }}</text>
|
||||
<text x="{{ x }}" y="{{ yDia - 22 }}" font-size="11" fill="#f472b6" text-anchor="middle"
|
||||
font-weight="500" class="font-sans">Diastolic: {{ diastolic[i] }}</text>
|
||||
</g>
|
||||
</g>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Average Lines -->
|
||||
{% set ySysAvg = 280 - (sys_avg / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: default;">
|
||||
<line x1="50" y1="{{ ySysAvg }}" x2="750" y2="{{ ySysAvg }}" stroke="#3b82f6" stroke-width="2"
|
||||
stroke-dasharray="8 6" opacity="0.8" />
|
||||
<line x1="50" y1="{{ ySysAvg }}" x2="750" y2="{{ ySysAvg }}" stroke="transparent"
|
||||
stroke-width="15" /> <!-- Invisible hover anchor -->
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="336" y="{{ ySysAvg - 34 }}" width="128" height="26" fill="#1f2937" rx="6" />
|
||||
<polygon points="394,{{ ySysAvg - 8 }} 406,{{ ySysAvg - 8 }} 400,{{ ySysAvg - 2 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="400" y="{{ ySysAvg - 17 }}" font-size="11" fill="#93c5fd" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">Avg Systolic: {{ sys_avg }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{% set yDiaAvg = 280 - (dia_avg / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: default;">
|
||||
<line x1="50" y1="{{ yDiaAvg }}" x2="750" y2="{{ yDiaAvg }}" stroke="#ec4899" stroke-width="2"
|
||||
stroke-dasharray="8 6" opacity="0.8" />
|
||||
<line x1="50" y1="{{ yDiaAvg }}" x2="750" y2="{{ yDiaAvg }}" stroke="transparent"
|
||||
stroke-width="15" /> <!-- Invisible hover anchor -->
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="336" y="{{ yDiaAvg - 34 }}" width="128" height="26" fill="#1f2937" rx="6" />
|
||||
<polygon points="394,{{ yDiaAvg - 8 }} 406,{{ yDiaAvg - 8 }} 400,{{ yDiaAvg - 2 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="400" y="{{ yDiaAvg - 17 }}" font-size="11" fill="#f472b6" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">Avg Diastolic: {{ dia_avg }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{% else %}
|
||||
<rect x="50" y="30" width="700" height="250" fill="#f9fafb" rx="10" />
|
||||
<text x="400" y="155" font-size="16" fill="#9ca3af" text-anchor="middle" font-weight="500">No data
|
||||
available for the selected period.</text>
|
||||
{% endif %}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Graph -->
|
||||
<!-- Heart Rate Graph Card -->
|
||||
<div class="bg-white rounded-2xl shadow-lg border border-gray-100 p-6 transition-all hover:shadow-xl">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-gray-600 mb-2">Heart Rate (bpm)</h3>
|
||||
<svg viewBox="0 0 600 250" xmlns="http://www.w3.org/2000/svg" class="w-full">
|
||||
<!-- Axes -->
|
||||
<line x1="50" y1="200" x2="550" y2="200" stroke="black" stroke-width="1" /> <!-- X-axis -->
|
||||
<line x1="50" y1="20" x2="50" y2="200" stroke="black" stroke-width="1" /> <!-- Y-axis -->
|
||||
<h3 class="text-xl font-bold text-gray-800">Heart Rate</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">Beats per minute over time (bpm)</p>
|
||||
</div>
|
||||
<div class="flex space-x-6 text-sm mt-4 md:mt-0 bg-gray-50 px-4 py-2 rounded-full">
|
||||
<div class="flex items-center"><span
|
||||
class="w-3 h-3 rounded-full bg-emerald-500 mr-2 shadow-sm"></span><span
|
||||
class="font-medium text-gray-700">Heart Rate</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Y-axis Labels (Heart Rate Values) -->
|
||||
<div class="w-full overflow-x-auto pb-4">
|
||||
<svg viewBox="0 0 800 350" class="w-full min-w-[600px] h-auto drop-shadow-sm font-sans"
|
||||
style="max-height: 350px;">
|
||||
<defs>
|
||||
<linearGradient id="hrGrad" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#10b981" stop-opacity="0.3" />
|
||||
<stop offset="100%" stop-color="#10b981" stop-opacity="0.0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{% if timestamps %}
|
||||
{% set n = timestamps|length %}
|
||||
{% set spacing = 700 / (n - 1) if n > 1 else 0 %}
|
||||
|
||||
<!-- Horizontal Grid Lines & Y-Axis Labels -->
|
||||
{% for value in range(50, 201, 50) %}
|
||||
<text x="40" y="{{ 200 - (value / 200 * 180) }}" font-size="10" text-anchor="end">{{ value }}</text>
|
||||
{% set y = 280 - (value / 200 * 250) %}
|
||||
<line x1="50" y1="{{ y }}" x2="750" y2="{{ y }}" stroke="#e5e7eb" stroke-width="1"
|
||||
stroke-dasharray="4 4" />
|
||||
<text x="40" y="{{ y + 4 }}" font-size="12" fill="#9ca3af" text-anchor="end" font-weight="500">{{ value
|
||||
}}</text>
|
||||
{% endfor %}
|
||||
|
||||
<!-- X-axis Labels (Timestamps) -->
|
||||
{% for i in range(timestamps|length) %}
|
||||
<text x="{{ 50 + i * 50 }}" y="215" font-size="10" text-anchor="middle">{{ timestamps[i] }}</text>
|
||||
<!-- X-Axis Base Line -->
|
||||
<line x1="50" y1="280" x2="750" y2="280" stroke="#d1d5db" stroke-width="1" />
|
||||
|
||||
<!-- X-Axis Labels -->
|
||||
{% set ns = namespace(last_x=-100) %}
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set is_last = (i == n - 1) %}
|
||||
{% if x - ns.last_x > 45 or is_last %}
|
||||
{% if not is_last or x - ns.last_x > 40 or ns.last_x == -100 %}
|
||||
<text x="{{ x }}" y="310" font-size="11" fill="#6b7280" text-anchor="middle" font-weight="500"
|
||||
transform="rotate(-25 {{ x }} 310)">{{
|
||||
timestamps[i] }}</text>
|
||||
{% set ns.last_x = x %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Heart Rate Filled Area -->
|
||||
<polygon fill="url(#hrGrad)" points="{% if n>1 %}50{% else %}400{% endif %},280
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (heart_rate[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}
|
||||
{% if n>1 %}{{ 50 + (n - 1) * spacing }}{% else %}400{% endif %},280" />
|
||||
|
||||
<!-- Heart Rate Line -->
|
||||
<polyline fill="none" stroke="green" stroke-width="2"
|
||||
points="{% for i in range(timestamps|length) %}{{ 50 + i * 50 }},{{ 200 - (heart_rate[i] / 200 * 180) }} {% endfor %}" />
|
||||
<polyline fill="none" stroke="#10b981" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"
|
||||
points="{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (heart_rate[i] / 200 * 250) %}
|
||||
{{ x }},{{ y }}
|
||||
{% endfor %}" />
|
||||
|
||||
<!-- Axis Labels -->
|
||||
<text x="25" y="110" font-size="12" transform="rotate(-90, 25, 110)" text-anchor="middle">Heart Rate
|
||||
(bpm)</text>
|
||||
<text x="300" y="240" font-size="12" text-anchor="middle">Date</text>
|
||||
<!-- Heart Rate Points -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set y = 280 - (heart_rate[i] / 200 * 250) %}
|
||||
<circle cx="{{ x }}" cy="{{ y }}" r="5" fill="#ffffff" stroke="#10b981" stroke-width="2.5"></circle>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Tooltips (Rendered last to stay on top) -->
|
||||
{% for i in range(n) %}
|
||||
{% set x = 50 + time_percentages[i] * 700 if n > 1 else 400 %}
|
||||
{% set yHR = 280 - (heart_rate[i] / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: pointer;">
|
||||
<circle cx="{{ x }}" cy="{{ yHR }}" r="15" fill="transparent" />
|
||||
<circle cx="{{ x }}" cy="{{ yHR }}" r="7" fill="#ffffff" stroke="#10b981" stroke-width="2.5"
|
||||
class="svg-tooltip-content" />
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="{{ x - 64 }}" y="{{ yHR - 54 }}" width="128" height="40" fill="#1f2937" rx="6" />
|
||||
<polygon points="{{ x - 6 }},{{ yHR - 14 }} {{ x + 6 }},{{ yHR - 14 }} {{ x }},{{ yHR - 6 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="{{ x }}" y="{{ yHR - 36 }}" font-size="11" fill="#f3f4f6" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">{{ timestamps[i] }}</text>
|
||||
<text x="{{ x }}" y="{{ yHR - 22 }}" font-size="11" fill="#34d399" text-anchor="middle"
|
||||
font-weight="500" class="font-sans">Heart Rate: {{ heart_rate[i] }}</text>
|
||||
</g>
|
||||
</g>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Average Line -->
|
||||
{% set yHrAvg = 280 - (hr_avg / 200 * 250) %}
|
||||
<g class="svg-tooltip-group" style="cursor: default;">
|
||||
<line x1="50" y1="{{ yHrAvg }}" x2="750" y2="{{ yHrAvg }}" stroke="#10b981" stroke-width="2"
|
||||
stroke-dasharray="8 6" opacity="0.8" />
|
||||
<line x1="50" y1="{{ yHrAvg }}" x2="750" y2="{{ yHrAvg }}" stroke="transparent" stroke-width="15" />
|
||||
<!-- Invisible hover anchor -->
|
||||
<g class="svg-tooltip-content">
|
||||
<rect x="336" y="{{ yHrAvg - 34 }}" width="128" height="26" fill="#1f2937" rx="6" />
|
||||
<polygon points="394,{{ yHrAvg - 8 }} 406,{{ yHrAvg - 8 }} 400,{{ yHrAvg - 2 }}"
|
||||
fill="#1f2937" />
|
||||
<text x="400" y="{{ yHrAvg - 17 }}" font-size="11" fill="#34d399" text-anchor="middle"
|
||||
font-weight="600" class="font-sans">Avg Heart Rate: {{ hr_avg }}</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
{% else %}
|
||||
<rect x="50" y="30" width="700" height="250" fill="#f9fafb" rx="10" />
|
||||
<text x="400" y="155" font-size="16" fill="#9ca3af" text-anchor="middle" font-weight="500">No data
|
||||
available for the selected period.</text>
|
||||
{% endif %}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user