Files
tatlock-ui/lib/shared/widgets/gauge_widget.dart
T
Jeroen SchweitzerandClaude Opus 4.5 9c9ec472ef feat(dashboard): add gauge, weather, and air quality widgets
- Create shared GaugeWidget with circular progress and customizable colors
- Add GaugeRow for displaying multiple gauges in a responsive layout
- Create WeatherWidget with temperature, condition, and details
- Create AirQualityWidget with AQI levels and pollutant readings
- Update DashboardContent with System Stats gauges and Environment section
- Both widgets use mock data, ready for API integration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 23:09:15 +01:00

222 lines
5.6 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
/// A circular gauge widget for displaying percentage values.
///
/// Commonly used for system stats like CPU, Memory, Disk usage.
class GaugeWidget extends StatelessWidget {
const GaugeWidget({
super.key,
required this.value,
required this.label,
this.size = 120,
this.strokeWidth = 10,
this.color,
this.backgroundColor,
this.showPercentage = true,
this.icon,
});
/// The value to display (0.0 - 1.0).
final double value;
/// Label displayed below the gauge.
final String label;
/// Size of the gauge widget.
final double size;
/// Width of the gauge arc stroke.
final double strokeWidth;
/// Color of the filled arc. Uses primary color if not specified.
final Color? color;
/// Color of the background arc. Uses surfaceVariant if not specified.
final Color? backgroundColor;
/// Whether to show the percentage text in the center.
final bool showPercentage;
/// Optional icon to show above the percentage.
final IconData? icon;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final effectiveColor = color ?? colorScheme.primary;
final effectiveBackgroundColor =
backgroundColor ?? colorScheme.surfaceContainerHighest;
// Clamp value between 0 and 1
final clampedValue = value.clamp(0.0, 1.0);
return SizedBox(
width: size,
height: size + 24, // Extra space for label
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size,
height: size,
child: CustomPaint(
painter: _GaugePainter(
value: clampedValue,
color: effectiveColor,
backgroundColor: effectiveBackgroundColor,
strokeWidth: strokeWidth,
),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(
icon,
size: size * 0.2,
color: effectiveColor,
),
SizedBox(height: size * 0.02),
],
if (showPercentage)
Text(
'${(clampedValue * 100).round()}%',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
fontSize: size * 0.18,
),
),
],
),
),
),
),
const SizedBox(height: 8),
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
class _GaugePainter extends CustomPainter {
_GaugePainter({
required this.value,
required this.color,
required this.backgroundColor,
required this.strokeWidth,
});
final double value;
final Color color;
final Color backgroundColor;
final double strokeWidth;
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width - strokeWidth) / 2;
// Start from top (-90 degrees) and sweep clockwise
const startAngle = -math.pi / 2;
const sweepAngle = 2 * math.pi;
// Background arc
final backgroundPaint = Paint()
..color = backgroundColor
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
backgroundPaint,
);
// Value arc
if (value > 0) {
final valuePaint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle * value,
false,
valuePaint,
);
}
}
@override
bool shouldRepaint(_GaugePainter oldDelegate) {
return oldDelegate.value != value ||
oldDelegate.color != color ||
oldDelegate.backgroundColor != backgroundColor ||
oldDelegate.strokeWidth != strokeWidth;
}
}
/// A row of gauge widgets with consistent sizing.
class GaugeRow extends StatelessWidget {
const GaugeRow({
super.key,
required this.gauges,
this.gaugeSize = 100,
});
final List<GaugeData> gauges;
final double gaugeSize;
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 16,
runSpacing: 16,
alignment: WrapAlignment.center,
children: gauges
.map(
(data) => GaugeWidget(
value: data.value,
label: data.label,
size: gaugeSize,
color: data.color,
icon: data.icon,
),
)
.toList(),
);
}
}
/// Data class for gauge configuration.
class GaugeData {
const GaugeData({
required this.value,
required this.label,
this.color,
this.icon,
});
final double value;
final String label;
final Color? color;
final IconData? icon;
}