354 lines
20 KiB
HTML
354 lines
20 KiB
HTML
<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">
|
|
<!-- Graph Date Filter -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-4 mb-2">
|
|
<form hx-get="{{ url_for('main.dashboard_graph') }}" hx-target="#dashboard-content"
|
|
class="flex flex-col md:flex-row gap-4 items-end">
|
|
<!-- Start Date -->
|
|
<div class="flex-1">
|
|
<label for="start_date" class="block text-sm font-medium text-gray-700 mb-1">Start Date</label>
|
|
<input type="date" name="start_date" id="start_date" value="{{ start_date or '' }}"
|
|
class="w-full p-2.5 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700">
|
|
</div>
|
|
|
|
<!-- End Date -->
|
|
<div class="flex-1">
|
|
<label for="end_date" class="block text-sm font-medium text-gray-700 mb-1">End Date</label>
|
|
<input type="date" name="end_date" id="end_date" value="{{ end_date or '' }}"
|
|
class="w-full p-2.5 border border-gray-300 rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-700">
|
|
</div>
|
|
|
|
<!-- Apply Button -->
|
|
<div>
|
|
<button type="submit"
|
|
class="w-full md:w-auto bg-primary-600 text-white px-6 py-2.5 rounded-xl font-semibold shadow-md hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors">
|
|
Apply Filter
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<!-- 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-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>
|
|
|
|
<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) %}
|
|
{% 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 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 %}
|
|
|
|
<!-- 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="#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="#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 %}" />
|
|
|
|
<!-- 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 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-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>
|
|
|
|
<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) %}
|
|
{% 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 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="#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 %}" />
|
|
|
|
<!-- 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> |