Files
orion/app/templates/shared/macros/charts.html
Samir Boulahtit 8ee8c398ce
Some checks failed
CI / ruff (push) Successful in 14s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
perf: add defer to scripts and lazy loading to images
Add defer attribute to 145 <script> tags across 103 template files
(PERF-067) and loading="lazy" to 22 <img> tags across 13 template
files (PERF-058). Both improve page load performance.

Validator totals: 0 errors, 2 warnings, 1360 info (down from 1527).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 20:55:52 +01:00

369 lines
11 KiB
HTML

{#
Chart Macros
============
Reusable chart components using Chart.js with Alpine.js integration.
Prerequisites:
Add Chart.js CDN to your base template:
<script defer src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
Usage:
{% from 'shared/macros/charts.html' import chart_card, line_chart, bar_chart, doughnut_chart %}
{# Line chart in a card #}
{{ chart_card('salesChart', 'Monthly Sales', 'line', chart_config) }}
{# Standalone bar chart #}
{{ bar_chart('ordersChart', chart_data, height='300px') }}
#}
{#
Chart Card
==========
A card container with a chart and optional dropdown menu.
Parameters:
- id: Unique chart ID (used for canvas element)
- title: Card title
- chart_type: 'line' | 'bar' | 'doughnut' | 'pie' (default: 'line')
- height: Chart height (default: '300px')
- show_menu: Whether to show dropdown menu (default: true)
- subtitle: Optional subtitle text
#}
{% macro chart_card(id, title, chart_type='line', height='300px', show_menu=true, subtitle=none) %}
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xs border border-gray-200 dark:border-gray-700 p-4 sm:p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-800 dark:text-white">{{ title }}</h3>
{% if subtitle %}
<p class="text-sm text-gray-500 dark:text-gray-400">{{ subtitle }}</p>
{% endif %}
</div>
{% if show_menu %}
<div x-data="{ menuOpen: false }" class="relative">
<button
@click="menuOpen = !menuOpen"
@click.outside="menuOpen = false"
class="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
</svg>
</button>
<div
x-show="menuOpen"
x-transition
class="absolute right-0 mt-1 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-10"
>
{{ caller() if caller is defined else '' }}
</div>
</div>
{% endif %}
</div>
<div style="height: {{ height }};">
<canvas id="{{ id }}"></canvas>
</div>
</div>
{% endmacro %}
{#
Line Chart
==========
A standalone line chart canvas.
Parameters:
- id: Unique chart ID
- height: Chart height (default: '300px')
- class_extra: Additional CSS classes
#}
{% macro line_chart(id, height='300px', class_extra='') %}
<div class="relative {{ class_extra }}" style="height: {{ height }};">
<canvas id="{{ id }}"></canvas>
</div>
{% endmacro %}
{#
Bar Chart
=========
A standalone bar chart canvas.
Parameters:
- id: Unique chart ID
- height: Chart height (default: '300px')
- class_extra: Additional CSS classes
#}
{% macro bar_chart(id, height='300px', class_extra='') %}
<div class="relative {{ class_extra }}" style="height: {{ height }};">
<canvas id="{{ id }}"></canvas>
</div>
{% endmacro %}
{#
Doughnut Chart
==============
A standalone doughnut/pie chart canvas.
Parameters:
- id: Unique chart ID
- size: Chart size (default: '200px')
- class_extra: Additional CSS classes
#}
{% macro doughnut_chart(id, size='200px', class_extra='') %}
<div class="relative inline-block {{ class_extra }}" style="width: {{ size }}; height: {{ size }};">
<canvas id="{{ id }}"></canvas>
</div>
{% endmacro %}
{#
Stats Chart Card
================
A card with a stat value and small sparkline chart.
Parameters:
- id: Unique chart ID
- title: Stat title
- value_var: Alpine.js variable for the value
- trend_var: Alpine.js variable for trend ('up' | 'down' | 'neutral')
- trend_value_var: Alpine.js variable for trend percentage
- chart_height: Sparkline height (default: '60px')
#}
{% macro stats_chart_card(id, title, value_var, trend_var=none, trend_value_var=none, chart_height='60px') %}
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xs border border-gray-200 dark:border-gray-700 p-4">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-500 dark:text-gray-400">{{ title }}</h4>
{% if trend_var %}
<span
class="inline-flex items-center text-xs font-medium"
:class="{
'text-green-600': {{ trend_var }} === 'up',
'text-red-600': {{ trend_var }} === 'down',
'text-gray-500': {{ trend_var }} === 'neutral'
}"
>
<svg
x-show="{{ trend_var }} === 'up'"
class="w-3 h-3 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"/>
</svg>
<svg
x-show="{{ trend_var }} === 'down'"
class="w-3 h-3 mr-1"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"/>
</svg>
<span x-text="{{ trend_value_var }}"></span>
</span>
{% endif %}
</div>
<p class="text-2xl font-bold text-gray-900 dark:text-white mb-3" x-text="{{ value_var }}"></p>
<div style="height: {{ chart_height }};">
<canvas id="{{ id }}"></canvas>
</div>
</div>
{% endmacro %}
{#
Chart Legend
============
A custom chart legend.
Parameters:
- items: List of legend items [{'label': '', 'color': ''}]
- layout: 'horizontal' | 'vertical' (default: 'horizontal')
#}
{% macro chart_legend(items, layout='horizontal') %}
<div class="flex {{ 'flex-wrap gap-4' if layout == 'horizontal' else 'flex-col gap-2' }} mt-4">
{% for item in items %}
<div class="flex items-center">
<span class="w-3 h-3 rounded-full mr-2" style="background-color: {{ item.color }};"></span>
<span class="text-sm text-gray-600 dark:text-gray-400">{{ item.label }}</span>
</div>
{% endfor %}
</div>
{% endmacro %}
{#
Chart.js Configuration Helper
=============================
JavaScript template for common chart configurations.
Include this in a <script> block and customize as needed.
#}
{% macro chart_config_script() %}
<script>
// Chart.js default configuration
Chart.defaults.font.family = "'Inter', 'Helvetica', 'Arial', sans-serif";
Chart.defaults.color = document.documentElement.classList.contains('dark') ? '#9CA3AF' : '#6B7280';
// Helper function to create responsive chart
function createChart(canvasId, type, data, options = {}) {
const ctx = document.getElementById(canvasId);
if (!ctx) return null;
const defaultOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: document.documentElement.classList.contains('dark') ? '#374151' : '#ffffff',
titleColor: document.documentElement.classList.contains('dark') ? '#F9FAFB' : '#111827',
bodyColor: document.documentElement.classList.contains('dark') ? '#D1D5DB' : '#4B5563',
borderColor: document.documentElement.classList.contains('dark') ? '#4B5563' : '#E5E7EB',
borderWidth: 1,
padding: 12,
cornerRadius: 8,
displayColors: true
}
}
};
return new Chart(ctx, {
type: type,
data: data,
options: { ...defaultOptions, ...options }
});
}
// Line chart preset
function createLineChart(canvasId, labels, datasets, options = {}) {
const defaultDatasetOptions = {
tension: 0.4,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 4,
fill: true
};
const formattedDatasets = datasets.map(ds => ({
...defaultDatasetOptions,
...ds
}));
return createChart(canvasId, 'line', { labels, datasets: formattedDatasets }, {
scales: {
x: {
grid: { display: false },
border: { display: false }
},
y: {
grid: {
color: document.documentElement.classList.contains('dark') ? '#374151' : '#F3F4F6'
},
border: { display: false },
beginAtZero: true
}
},
...options
});
}
// Bar chart preset
function createBarChart(canvasId, labels, datasets, options = {}) {
return createChart(canvasId, 'bar', { labels, datasets }, {
scales: {
x: {
grid: { display: false },
border: { display: false }
},
y: {
grid: {
color: document.documentElement.classList.contains('dark') ? '#374151' : '#F3F4F6'
},
border: { display: false },
beginAtZero: true
}
},
...options
});
}
// Doughnut chart preset
function createDoughnutChart(canvasId, labels, data, colors, options = {}) {
return createChart(canvasId, 'doughnut', {
labels,
datasets: [{
data,
backgroundColor: colors,
borderWidth: 0,
hoverOffset: 4
}]
}, {
cutout: '70%',
...options
});
}
// Color presets
const chartColors = {
purple: {
solid: '#9333EA',
light: 'rgba(147, 51, 234, 0.1)'
},
blue: {
solid: '#3B82F6',
light: 'rgba(59, 130, 246, 0.1)'
},
green: {
solid: '#10B981',
light: 'rgba(16, 185, 129, 0.1)'
},
red: {
solid: '#EF4444',
light: 'rgba(239, 68, 68, 0.1)'
},
yellow: {
solid: '#F59E0B',
light: 'rgba(245, 158, 11, 0.1)'
},
gray: {
solid: '#6B7280',
light: 'rgba(107, 114, 128, 0.1)'
}
};
</script>
{% endmacro %}
{#
Example Usage Comment
=====================
Copy this to your page to see how to use the chart macros:
{% from 'shared/macros/charts.html' import chart_card, chart_config_script %}
{{ chart_card('monthlySales', 'Monthly Sales', 'line') }}
{{ chart_config_script() }}
<script>
document.addEventListener('DOMContentLoaded', function() {
createLineChart('monthlySales',
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
[{
label: 'Sales',
data: [30, 40, 35, 50, 49, 60],
borderColor: chartColors.purple.solid,
backgroundColor: chartColors.purple.light
}]
);
});
</script>
#}