Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
224
lib/Peach/designs/ActivityChart.dart
Normal file
224
lib/Peach/designs/ActivityChart.dart
Normal file
@@ -0,0 +1,224 @@
|
||||
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'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user