225 lines
6.5 KiB
Dart
225 lines
6.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../about/internal/application/TextType.dart';
|
|
|
|
import '../about/internal/application/DeploymentRecord.dart';
|
|
import '../utils/Colors.dart';
|
|
import 'text/Text.dart';
|
|
|
|
enum _Range { week, month, year }
|
|
|
|
class _Bucket {
|
|
final String label;
|
|
final int count;
|
|
_Bucket(this.label, this.count);
|
|
}
|
|
|
|
/// "Compute Activity" style card: a bar chart of deployment counts over time,
|
|
/// with the peak column highlighted, and a Week / Month / Year toggle.
|
|
class ActivityChart extends StatefulWidget {
|
|
final List<DeploymentRecord> history;
|
|
const ActivityChart({super.key, required this.history});
|
|
|
|
@override
|
|
State<ActivityChart> createState() => _ActivityChartState();
|
|
}
|
|
|
|
class _ActivityChartState extends State<ActivityChart> {
|
|
_Range _range = _Range.week;
|
|
|
|
List<_Bucket> _buckets() {
|
|
final now = DateTime.now();
|
|
final h = widget.history;
|
|
int between(DateTime a, DateTime b) =>
|
|
h.where((r) => !r.time.isBefore(a) && r.time.isBefore(b)).length;
|
|
const wd = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
const mo = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec',
|
|
];
|
|
switch (_range) {
|
|
case _Range.week:
|
|
final today = DateTime(now.year, now.month, now.day);
|
|
return List.generate(7, (i) {
|
|
final d = today.subtract(Duration(days: 6 - i));
|
|
return _Bucket(
|
|
wd[d.weekday - 1],
|
|
between(d, d.add(const Duration(days: 1))),
|
|
);
|
|
});
|
|
case _Range.month:
|
|
// last 6 weeks
|
|
final startOfWeek = DateTime(
|
|
now.year,
|
|
now.month,
|
|
now.day,
|
|
).subtract(Duration(days: now.weekday - 1));
|
|
return List.generate(6, (i) {
|
|
final s = startOfWeek.subtract(Duration(days: (5 - i) * 7));
|
|
final e = s.add(const Duration(days: 7));
|
|
return _Bucket('${s.month}/${s.day}', between(s, e));
|
|
});
|
|
case _Range.year:
|
|
// last 6 months
|
|
return List.generate(6, (i) {
|
|
final m = DateTime(now.year, now.month - (5 - i), 1);
|
|
final next = DateTime(m.year, m.month + 1, 1);
|
|
return _Bucket(mo[m.month - 1], between(m, next));
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final jp = context.jp;
|
|
final buckets = _buckets();
|
|
final max = buckets.fold(0, (m, b) => b.count > m ? b.count : m);
|
|
final peakIdx = buckets.indexWhere((b) => b.count == max && max > 0);
|
|
final total = buckets.fold(0, (n, b) => n + b.count);
|
|
return Container(
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: PeachTheme.panel(context),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
text(
|
|
'Deployment activity',
|
|
16,
|
|
TextType.Bold,
|
|
color: jp.text,
|
|
weight: FontWeight.w800,
|
|
),
|
|
const SizedBox(height: 2),
|
|
text(
|
|
'$total deploys this period',
|
|
12.5,
|
|
TextType.Regular,
|
|
color: jp.textDim,
|
|
weight: FontWeight.w400,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_toggle(jp),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
SizedBox(
|
|
height: 190,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
for (var i = 0; i < buckets.length; i++)
|
|
Expanded(child: _bar(jp, buckets[i], max, i == peakIdx)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _bar(JpColors jp, _Bucket b, int max, bool peak) {
|
|
final frac = max == 0 ? 0.0 : b.count / max;
|
|
final h = 12 + frac * 130;
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
text(
|
|
'${b.count}',
|
|
12,
|
|
TextType.Regular,
|
|
color: peak ? PeachTheme.seed : jp.textDim,
|
|
weight: FontWeight.w800,
|
|
),
|
|
const SizedBox(height: 6),
|
|
Container(
|
|
width: 26,
|
|
height: h,
|
|
decoration: BoxDecoration(
|
|
gradient: peak ? PeachTheme.brandGradient : null,
|
|
color: peak ? null : jp.surface2,
|
|
borderRadius: BorderRadius.circular(9),
|
|
boxShadow: peak
|
|
? PeachTheme.glow(PeachTheme.seed, opacity: 0.35, blur: 16)
|
|
: null,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
text(
|
|
b.label,
|
|
11,
|
|
TextType.Regular,
|
|
color: peak ? jp.text : jp.textFaint,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.clip,
|
|
weight: peak ? FontWeight.w700 : FontWeight.w500,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _toggle(JpColors jp) {
|
|
Widget seg(_Range r, String label) {
|
|
final active = _range == r;
|
|
return GestureDetector(
|
|
onTap: () => setState(() => _range = r),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
|
decoration: BoxDecoration(
|
|
color: active ? jp.surface : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(9),
|
|
boxShadow: active
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.08),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: text(
|
|
label,
|
|
12.5,
|
|
TextType.Regular,
|
|
color: active ? jp.text : jp.textDim,
|
|
weight: active ? FontWeight.w700 : FontWeight.w500,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: jp.surface2,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
seg(_Range.week, 'Week'),
|
|
seg(_Range.month, 'Month'),
|
|
seg(_Range.year, 'Year'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|