Some checks failed
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>
369 lines
11 KiB
HTML
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>
|
|
#}
|