Created
January 20, 2026 20:11
-
-
Save JaredEzz/ad2678cc55cec7fbeee2169b5bdb568a to your computer and use it in GitHub Desktop.
Bloc Refactor Evaluation Metrics
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/material.dart'; | |
| import 'dart:math' as math; | |
| import 'dart:ui' as ui; | |
| // ============================================================================== | |
| // 1. DATA MODELS & MOCK DATA | |
| // ============================================================================== | |
| class BlocMetrics { | |
| final String name; | |
| final String domain; // e.g., "Feature", "Core", "Legacy" | |
| // Categorical Scores (0-100) | |
| final double modularization; | |
| final double dataIntegrity; | |
| final double statePattern; | |
| final double errorHandling; | |
| final double reliability; // Testing coverage | |
| final double decoupling; | |
| // Complexity Magnitude (M) | |
| final double magnitude; | |
| const BlocMetrics({ | |
| required this.name, | |
| required this.domain, | |
| required this.modularization, | |
| required this.dataIntegrity, | |
| required this.statePattern, | |
| required this.errorHandling, | |
| required this.reliability, | |
| required this.decoupling, | |
| required this.magnitude, | |
| }); | |
| double get avgHealth => (modularization + dataIntegrity + statePattern + errorHandling + reliability + decoupling) / 6; | |
| // The "Misery Index" - How urgent is the refactor? | |
| // High Magnitude + Low Health = High Misery | |
| double get miseryIndex => magnitude * (1 - (avgHealth / 100)); | |
| Color get healthColor { | |
| if (avgHealth >= 90) return const Color(0xFF00C853); // A - Perfect | |
| if (avgHealth >= 75) return const Color(0xFF64DD17); // B - Good | |
| if (avgHealth >= 60) return const Color(0xFFFFD600); // C - Warning | |
| if (avgHealth >= 40) return const Color(0xFFFF6D00); // D - Danger | |
| return const Color(0xFFD50000); // F - Critical | |
| } | |
| } | |
| class Snapshot { | |
| final String label; | |
| final List<BlocMetrics> blocs; | |
| Snapshot(this.label, this.blocs); | |
| } | |
| // --- MOCK REPOSITORY --- | |
| class Repository { | |
| static List<Snapshot> getHistory() { | |
| return [ | |
| Snapshot("Q3 2025 (Legacy)", [ | |
| const BlocMetrics(name: "SimulatorBloc", domain: "Core", magnitude: 580, modularization: 20, dataIntegrity: 30, statePattern: 20, errorHandling: 10, reliability: 15, decoupling: 10), | |
| const BlocMetrics(name: "AssessmentBloc", domain: "Core", magnitude: 420, modularization: 40, dataIntegrity: 40, statePattern: 50, errorHandling: 30, reliability: 20, decoupling: 30), | |
| const BlocMetrics(name: "AuthBloc", domain: "Shared", magnitude: 150, modularization: 80, dataIntegrity: 70, statePattern: 60, errorHandling: 50, reliability: 70, decoupling: 60), | |
| const BlocMetrics(name: "ReportingBloc", domain: "Feature", magnitude: 310, modularization: 30, dataIntegrity: 20, statePattern: 40, errorHandling: 20, reliability: 0, decoupling: 20), | |
| const BlocMetrics(name: "UserProfileBloc", domain: "Feature", magnitude: 90, modularization: 90, dataIntegrity: 85, statePattern: 90, errorHandling: 80, reliability: 90, decoupling: 90), | |
| const BlocMetrics(name: "WizardBloc", domain: "Feature", magnitude: 220, modularization: 50, dataIntegrity: 60, statePattern: 50, errorHandling: 50, reliability: 40, decoupling: 50), | |
| ]), | |
| Snapshot("Q4 2025 (Current)", [ | |
| const BlocMetrics(name: "SimulatorBloc", domain: "Core", magnitude: 510, modularization: 35, dataIntegrity: 50, statePattern: 30, errorHandling: 20, reliability: 40, decoupling: 25), // Slight improvement | |
| const BlocMetrics(name: "AssessmentBloc", domain: "Core", magnitude: 280, modularization: 75, dataIntegrity: 80, statePattern: 85, errorHandling: 70, reliability: 80, decoupling: 75), // Refactored! | |
| const BlocMetrics(name: "AuthBloc", domain: "Shared", magnitude: 140, modularization: 90, dataIntegrity: 90, statePattern: 85, errorHandling: 90, reliability: 95, decoupling: 90), | |
| const BlocMetrics(name: "ReportingBloc", domain: "Feature", magnitude: 320, modularization: 30, dataIntegrity: 20, statePattern: 40, errorHandling: 20, reliability: 0, decoupling: 20), // Stagnant | |
| const BlocMetrics(name: "UserProfileBloc", domain: "Feature", magnitude: 95, modularization: 95, dataIntegrity: 90, statePattern: 95, errorHandling: 90, reliability: 100, decoupling: 95), | |
| const BlocMetrics(name: "WizardBloc", domain: "Feature", magnitude: 230, modularization: 55, dataIntegrity: 65, statePattern: 55, errorHandling: 55, reliability: 45, decoupling: 55), | |
| const BlocMetrics(name: "NewSearchBloc", domain: "Feature", magnitude: 80, modularization: 100, dataIntegrity: 100, statePattern: 100, errorHandling: 100, reliability: 100, decoupling: 100), // New perfect bloc | |
| ]), | |
| ]; | |
| } | |
| } | |
| // ============================================================================== | |
| // 2. MAIN APP SCAFFOLDING | |
| // ============================================================================== | |
| void main() => runApp(const BlocAuditApp()); | |
| class BlocAuditApp extends StatelessWidget { | |
| const BlocAuditApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| debugShowCheckedModeBanner: false, | |
| theme: ThemeData.dark().copyWith( | |
| scaffoldBackgroundColor: const Color(0xFF0F172A), // Slate 900 | |
| cardColor: const Color(0xFF1E293B), // Slate 800 | |
| colorScheme: const ColorScheme.dark( | |
| primary: Color(0xFF38BDF8), // Sky 400 | |
| secondary: Color(0xFF818CF8), // Indigo 400 | |
| surface: Color(0xFF1E293B), | |
| ), | |
| ), | |
| home: const DashboardLayout(), | |
| ); | |
| } | |
| } | |
| class DashboardLayout extends StatefulWidget { | |
| const DashboardLayout({super.key}); | |
| @override | |
| State<DashboardLayout> createState() => _DashboardLayoutState(); | |
| } | |
| class _DashboardLayoutState extends State<DashboardLayout> { | |
| final List<Snapshot> history = Repository.getHistory(); | |
| late Snapshot currentSnapshot; | |
| late Snapshot? previousSnapshot; | |
| int _selectedIndex = 0; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| currentSnapshot = history.last; | |
| previousSnapshot = history.first; | |
| } | |
| void _switchSnapshot(Snapshot s) { | |
| setState(() { | |
| // If we pick the first one, prev is null. If we pick last, prev is first. | |
| // Simplified for demo logic. | |
| if (s == history.first) { | |
| currentSnapshot = s; | |
| previousSnapshot = null; | |
| } else { | |
| currentSnapshot = s; | |
| previousSnapshot = history.first; | |
| } | |
| }); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| // The View definitions | |
| final views = [ | |
| _ViewDef("Priority Heatmap", Icons.dashboard, HeatmapView(currentSnapshot)), | |
| _ViewDef("Quadrant Scatter", Icons.bubble_chart, ScatterView(currentSnapshot)), | |
| _ViewDef("Radar Analysis", Icons.radar, RadarView(currentSnapshot)), | |
| _ViewDef("Progress Delta", Icons.compare_arrows, DeltaView(currentSnapshot, previousSnapshot)), | |
| _ViewDef("Awfulness Ranked", Icons.warning_amber, AwfulnessListView(currentSnapshot)), | |
| _ViewDef("Modularization Tree", Icons.account_tree, TreeView(currentSnapshot)), | |
| _ViewDef("Category Breakdown", Icons.bar_chart, CategoryBreakdownView(currentSnapshot)), | |
| _ViewDef("Historic Trend", Icons.show_chart, TrendLineView(history)), | |
| _ViewDef("Data Grid", Icons.table_chart, DataGridView(currentSnapshot)), | |
| _ViewDef("Grade Distribution", Icons.pie_chart, DistributionView(currentSnapshot)), | |
| ]; | |
| return Scaffold( | |
| appBar: AppBar( | |
| title: const Text("Birdseye BLoC Auditor"), | |
| elevation: 0, | |
| backgroundColor: const Color(0xFF0F172A), | |
| actions: [ | |
| PopupMenuButton<Snapshot>( | |
| initialValue: currentSnapshot, | |
| onSelected: _switchSnapshot, | |
| itemBuilder: (context) => history.map((s) => PopupMenuItem(value: s, child: Text(s.label))).toList(), | |
| child: Padding( | |
| padding: const EdgeInsets.symmetric(horizontal: 16.0), | |
| child: Row(children: [Text(currentSnapshot.label), const Icon(Icons.arrow_drop_down)]), | |
| ), | |
| ), | |
| ], | |
| ), | |
| body: Row( | |
| children: [ | |
| NavigationRail( | |
| backgroundColor: const Color(0xFF1E293B), | |
| selectedIndex: _selectedIndex, | |
| onDestinationSelected: (int index) => setState(() => _selectedIndex = index), | |
| labelType: NavigationRailLabelType.selected, | |
| destinations: views.map((v) => NavigationRailDestination( | |
| icon: Icon(v.icon), | |
| label: Text(v.title, style: const TextStyle(fontSize: 10)), | |
| )).toList(), | |
| ), | |
| const VerticalDivider(thickness: 1, width: 1, color: Colors.white10), | |
| Expanded( | |
| child: Padding( | |
| padding: const EdgeInsets.all(24.0), | |
| child: views[_selectedIndex].widget, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class _ViewDef { | |
| final String title; | |
| final IconData icon; | |
| final Widget widget; | |
| _ViewDef(this.title, this.icon, this.widget); | |
| } | |
| // ============================================================================== | |
| // 3. VISUALIZATION IMPLEMENTATIONS | |
| // ============================================================================== | |
| /// --- 1. PRIORITY HEATMAP (Treemap Style) --- | |
| /// Concepts: Size = Magnitude, Color = Health | |
| class HeatmapView extends StatelessWidget { | |
| final Snapshot snapshot; | |
| const HeatmapView(this.snapshot, {super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| // Sort by magnitude desc | |
| final sorted = List<BlocMetrics>.from(snapshot.blocs)..sort((a, b) => b.magnitude.compareTo(a.magnitude)); | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const _Header("Priority Heatmap", "Size = Complexity (Magnitude) | Color = Health Score"), | |
| Expanded( | |
| child: LayoutBuilder( | |
| builder: (context, constraints) { | |
| // Simple "Squarified" Treemap approximation using Wrap for simplicity in pure Flutter | |
| // In a real app, you'd use a proper treemap algo, but this visualized the concept well. | |
| return SingleChildScrollView( | |
| child: Wrap( | |
| spacing: 4, | |
| runSpacing: 4, | |
| children: sorted.map((bloc) { | |
| // Normalizing size to viewport | |
| final areaPercent = bloc.magnitude / 2000; // rough total mag | |
| final boxSize = math.sqrt(constraints.maxWidth * constraints.maxHeight * areaPercent).clamp(60.0, 300.0); | |
| return Tooltip( | |
| message: "${bloc.name}\nMag: ${bloc.magnitude}\nHealth: ${bloc.avgHealth.toInt()}%", | |
| child: Container( | |
| width: boxSize, | |
| height: boxSize, | |
| padding: const EdgeInsets.all(8), | |
| decoration: BoxDecoration( | |
| color: bloc.healthColor.withOpacity(0.8), | |
| borderRadius: BorderRadius.circular(4), | |
| border: Border.all(color: Colors.white24), | |
| ), | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Text(bloc.name, textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: Colors.black87), maxLines: 2, overflow: TextOverflow.ellipsis), | |
| const SizedBox(height: 4), | |
| Text("${bloc.avgHealth.toInt()}%", style: const TextStyle(fontWeight: FontWeight.w900, color: Colors.black54)), | |
| ], | |
| ), | |
| ), | |
| ); | |
| }).toList(), | |
| ), | |
| ); | |
| }, | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| /// --- 2. QUADRANT SCATTER (Bubble Chart) --- | |
| /// X = Magnitude, Y = Health. | |
| /// Top Right = Complex but Good (Gold Standard). Bottom Right = Complex & Bad (Critical). | |
| class ScatterView extends StatelessWidget { | |
| final Snapshot snapshot; | |
| const ScatterView(this.snapshot, {super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const _Header("Compliance vs Complexity", "Y-Axis: Health (High is Good) | X-Axis: Magnitude (Right is Complex)"), | |
| Expanded( | |
| child: Container( | |
| padding: const EdgeInsets.all(16), | |
| decoration: BoxDecoration( | |
| border: Border.all(color: Colors.white10), | |
| borderRadius: BorderRadius.circular(12), | |
| color: Colors.black12, | |
| ), | |
| child: CustomPaint( | |
| size: Size.infinite, | |
| painter: _ScatterPainter(snapshot.blocs), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| class _ScatterPainter extends CustomPainter { | |
| final List<BlocMetrics> blocs; | |
| _ScatterPainter(this.blocs); | |
| @override | |
| void paint(Canvas canvas, Size size) { | |
| final paint = Paint()..style = PaintingStyle.fill; | |
| final textPainter = TextPainter(textDirection: TextDirection.ltr); | |
| // Grid | |
| paint.color = Colors.white10; | |
| paint.strokeWidth = 1; | |
| canvas.drawLine(Offset(0, size.height/2), Offset(size.width, size.height/2), paint); // Mid Health | |
| canvas.drawLine(Offset(size.width/2, 0), Offset(size.width/2, size.height), paint); // Mid Mag | |
| // Plot | |
| const maxMag = 600.0; | |
| for (var bloc in blocs) { | |
| final x = (bloc.magnitude / maxMag).clamp(0.0, 1.0) * size.width; | |
| final y = size.height - ((bloc.avgHealth / 100) * size.height); | |
| paint.color = bloc.healthColor.withOpacity(0.6); | |
| canvas.drawCircle(Offset(x, y), 12, paint); | |
| paint.color = Colors.white; // border | |
| paint.style = PaintingStyle.stroke; | |
| canvas.drawCircle(Offset(x, y), 12, paint); | |
| paint.style = PaintingStyle.fill; | |
| // Label | |
| textPainter.text = TextSpan(text: bloc.name, style: const TextStyle(color: Colors.white70, fontSize: 10)); | |
| textPainter.layout(); | |
| textPainter.paint(canvas, Offset(x + 15, y - 5)); | |
| } | |
| } | |
| @override | |
| bool shouldRepaint(covariant CustomPainter oldDelegate) => true; | |
| } | |
| /// --- 3. RADAR ANALYSIS (Spider Chart) --- | |
| /// Compare the 6 axes for a selected Bloc | |
| class RadarView extends StatefulWidget { | |
| final Snapshot snapshot; | |
| const RadarView(this.snapshot, {super.key}); | |
| @override | |
| State<RadarView> createState() => _RadarViewState(); | |
| } | |
| class _RadarViewState extends State<RadarView> { | |
| BlocMetrics? selected; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| selected = widget.snapshot.blocs.first; | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Column( | |
| children: [ | |
| const _Header("Categorical Radar", "Select a BLoC to analyze its specific weaknesses."), | |
| SizedBox( | |
| height: 50, | |
| child: ListView( | |
| scrollDirection: Axis.horizontal, | |
| children: widget.snapshot.blocs.map((b) => Padding( | |
| padding: const EdgeInsets.only(right: 8.0), | |
| child: FilterChip( | |
| label: Text(b.name), | |
| selected: selected == b, | |
| onSelected: (v) => setState(() => selected = b), | |
| ), | |
| )).toList(), | |
| ), | |
| ), | |
| const SizedBox(height: 20), | |
| Expanded( | |
| child: Center( | |
| child: SizedBox( | |
| width: 400, | |
| height: 400, | |
| child: CustomPaint( | |
| painter: _RadarPainter(selected!), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| class _RadarPainter extends CustomPainter { | |
| final BlocMetrics bloc; | |
| _RadarPainter(this.bloc); | |
| @override | |
| void paint(Canvas canvas, Size size) { | |
| final center = Offset(size.width / 2, size.height / 2); | |
| final radius = size.width / 2 * 0.8; | |
| final paint = Paint()..color = Colors.white24..style = PaintingStyle.stroke; | |
| // 6 Axes | |
| final axes = ["Modular", "Data", "State", "Error", "Tests", "Decouple"]; | |
| final scores = [bloc.modularization, bloc.dataIntegrity, bloc.statePattern, bloc.errorHandling, bloc.reliability, bloc.decoupling]; | |
| // Draw Web | |
| for (int i = 1; i <= 4; i++) { | |
| canvas.drawCircle(center, radius * (i / 4), paint); | |
| } | |
| final path = Path(); | |
| for (int i = 0; i < 6; i++) { | |
| final angle = (i * 60 - 90) * (math.pi / 180); | |
| final x = center.dx + radius * math.cos(angle); | |
| final y = center.dy + radius * math.sin(angle); | |
| canvas.drawLine(center, Offset(x, y), paint); | |
| // Draw Label | |
| final tp = TextPainter(text: TextSpan(text: axes[i], style: const TextStyle(color: Colors.white54, fontSize: 12)), textDirection: TextDirection.ltr); | |
| tp.layout(); | |
| tp.paint(canvas, Offset(center.dx + (radius + 20) * math.cos(angle) - tp.width/2, center.dy + (radius + 20) * math.sin(angle) - tp.height/2)); | |
| // Path calc | |
| final scoreRadius = radius * (scores[i] / 100); | |
| final sx = center.dx + scoreRadius * math.cos(angle); | |
| final sy = center.dy + scoreRadius * math.sin(angle); | |
| if (i == 0) path.moveTo(sx, sy); else path.lineTo(sx, sy); | |
| } | |
| path.close(); | |
| // Fill Shape | |
| paint.color = bloc.healthColor.withOpacity(0.3); | |
| paint.style = PaintingStyle.fill; | |
| canvas.drawPath(path, paint); | |
| paint.color = bloc.healthColor; | |
| paint.strokeWidth = 2; | |
| paint.style = PaintingStyle.stroke; | |
| canvas.drawPath(path, paint); | |
| } | |
| @override | |
| bool shouldRepaint(covariant CustomPainter oldDelegate) => true; | |
| } | |
| /// --- 4. PROGRESS DELTA (Comparison) --- | |
| class DeltaView extends StatelessWidget { | |
| final Snapshot current; | |
| final Snapshot? previous; | |
| const DeltaView(this.current, this.previous, {super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| if (previous == null) return const Center(child: Text("Select a different snapshot to compare against first quarter.")); | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| _Header("Refactor Impact", "Comparing ${previous!.label} vs ${current.label}"), | |
| Expanded( | |
| child: ListView.builder( | |
| itemCount: current.blocs.length, | |
| itemBuilder: (context, index) { | |
| final cur = current.blocs[index]; | |
| // Find matching prev | |
| final prev = previous!.blocs.firstWhere((p) => p.name == cur.name, orElse: () => cur); | |
| final delta = cur.avgHealth - prev.avgHealth; | |
| return Card( | |
| margin: const EdgeInsets.only(bottom: 8), | |
| child: Padding( | |
| padding: const EdgeInsets.all(16.0), | |
| child: Row( | |
| children: [ | |
| SizedBox(width: 150, child: Text(cur.name, style: const TextStyle(fontWeight: FontWeight.bold))), | |
| Expanded( | |
| child: Column( | |
| children: [ | |
| Stack( | |
| children: [ | |
| Container(height: 8, color: Colors.white10), | |
| FractionallySizedBox(widthFactor: prev.avgHealth / 100, child: Container(height: 8, color: Colors.grey)), | |
| FractionallySizedBox(widthFactor: cur.avgHealth / 100, child: Container(height: 8, color: cur.healthColor.withOpacity(0.5))), | |
| ], | |
| ) | |
| ], | |
| ), | |
| ), | |
| const SizedBox(width: 20), | |
| Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), | |
| decoration: BoxDecoration( | |
| color: delta > 0 ? Colors.green.withOpacity(0.2) : (delta < 0 ? Colors.red.withOpacity(0.2) : Colors.grey.withOpacity(0.2)), | |
| borderRadius: BorderRadius.circular(12) | |
| ), | |
| child: Text( | |
| "${delta > 0 ? '+' : ''}${delta.toStringAsFixed(1)}%", | |
| style: TextStyle(color: delta > 0 ? Colors.green : (delta < 0 ? Colors.red : Colors.grey), fontWeight: FontWeight.bold) | |
| ), | |
| ) | |
| ], | |
| ), | |
| ), | |
| ); | |
| }, | |
| ), | |
| ) | |
| ], | |
| ); | |
| } | |
| } | |
| /// --- 5. AWFULNESS RANKING --- | |
| class AwfulnessListView extends StatelessWidget { | |
| final Snapshot snapshot; | |
| const AwfulnessListView(this.snapshot, {super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| final sorted = List<BlocMetrics>.from(snapshot.blocs)..sort((a, b) => b.miseryIndex.compareTo(a.miseryIndex)); | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const _Header("Refactor Priority Queue", "Sorted by 'Misery Index' (Size * Inverted Health)"), | |
| Expanded( | |
| child: ListView.separated( | |
| itemCount: sorted.length, | |
| separatorBuilder: (_, __) => const Divider(height: 1), | |
| itemBuilder: (context, index) { | |
| final b = sorted[index]; | |
| return ListTile( | |
| leading: CircleAvatar( | |
| backgroundColor: index < 3 ? Colors.redAccent : Colors.grey[800], | |
| child: Text("${index + 1}", style: const TextStyle(color: Colors.white)), | |
| ), | |
| title: Text(b.name, style: const TextStyle(fontWeight: FontWeight.bold)), | |
| subtitle: Text("Mag: ${b.magnitude.toInt()} | Health: ${b.avgHealth.toInt()}%"), | |
| trailing: Text("Score: ${b.miseryIndex.toInt()}", style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: Colors.white54)), | |
| ); | |
| }, | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| /// --- 6. MODULARIZATION TREE (Visual Hierarchy) --- | |
| /// Simulates how blocs break down. | |
| class TreeView extends StatelessWidget { | |
| final Snapshot snapshot; | |
| const TreeView(this.snapshot, {super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| // Group by Domain | |
| final domains = <String, List<BlocMetrics>>{}; | |
| for (var b in snapshot.blocs) { | |
| domains.putIfAbsent(b.domain, () => []).add(b); | |
| } | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const _Header("Architecture Tree", "Visualizing Domain clusters."), | |
| Expanded( | |
| child: SingleChildScrollView( | |
| child: Column( | |
| children: domains.entries.map((entry) { | |
| return ExpansionTile( | |
| initiallyExpanded: true, | |
| leading: const Icon(Icons.folder_open, color: Colors.blueGrey), | |
| title: Text(entry.key, style: const TextStyle(fontWeight: FontWeight.bold)), | |
| children: entry.value.map((b) => ListTile( | |
| contentPadding: const EdgeInsets.only(left: 40, right: 20), | |
| leading: Icon(Icons.extension, color: b.healthColor), | |
| title: Text(b.name), | |
| trailing: SizedBox( | |
| width: 60, | |
| child: LinearProgressIndicator(value: b.avgHealth / 100, color: b.healthColor, backgroundColor: Colors.white10), | |
| ), | |
| )).toList(), | |
| ); | |
| }).toList(), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| /// --- 7. CATEGORY BREAKDOWN (Aggregate) --- | |
| class CategoryBreakdownView extends StatelessWidget { | |
| final Snapshot snapshot; | |
| const CategoryBreakdownView(this.snapshot, {super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| // Calculate averages per category | |
| double avg(double Function(BlocMetrics) selector) { | |
| return snapshot.blocs.map(selector).reduce((a, b) => a + b) / snapshot.blocs.length; | |
| } | |
| final data = [ | |
| MapEntry("Modularization", avg((b) => b.modularization)), | |
| MapEntry("Data Integrity", avg((b) => b.dataIntegrity)), | |
| MapEntry("State Pattern", avg((b) => b.statePattern)), | |
| MapEntry("Error Handling", avg((b) => b.errorHandling)), | |
| MapEntry("Reliability", avg((b) => b.reliability)), | |
| MapEntry("Decoupling", avg((b) => b.decoupling)), | |
| ]; | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const _Header("Team Weaknesses", "Aggregate scores across the entire codebase."), | |
| Expanded( | |
| child: Padding( | |
| padding: const EdgeInsets.only(top: 20.0), | |
| child: ListView( | |
| children: data.map((e) => Padding( | |
| padding: const EdgeInsets.only(bottom: 16.0), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| children: [ | |
| Text(e.key, style: const TextStyle(fontSize: 16)), | |
| Text("${e.value.toInt()}%", style: const TextStyle(fontWeight: FontWeight.bold)), | |
| ], | |
| ), | |
| const SizedBox(height: 8), | |
| ClipRRect( | |
| borderRadius: BorderRadius.circular(4), | |
| child: LinearProgressIndicator( | |
| value: e.value / 100, | |
| minHeight: 12, | |
| backgroundColor: Colors.white10, | |
| color: e.value > 80 ? Colors.green : (e.value > 60 ? Colors.yellow : Colors.red), | |
| ), | |
| ) | |
| ], | |
| ), | |
| )).toList(), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| /// --- 8. HISTORIC TREND --- | |
| class TrendLineView extends StatelessWidget { | |
| final List<Snapshot> history; | |
| const TrendLineView(this.history, {super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const _Header("Quality Velocity", "Average Codebase Health over Time"), | |
| Expanded( | |
| child: Center( | |
| child: Container( | |
| height: 300, | |
| width: double.infinity, | |
| padding: const EdgeInsets.all(20), | |
| child: CustomPaint( | |
| painter: _TrendPainter(history), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| class _TrendPainter extends CustomPainter { | |
| final List<Snapshot> history; | |
| _TrendPainter(this.history); | |
| @override | |
| void paint(Canvas canvas, Size size) { | |
| final paint = Paint()..color = Colors.blue..strokeWidth = 4..style = PaintingStyle.stroke; | |
| final dotPaint = Paint()..color = Colors.white; | |
| final path = Path(); | |
| double getAvg(Snapshot s) => s.blocs.map((b) => b.avgHealth).reduce((a, b) => a + b) / s.blocs.length; | |
| final points = <Offset>[]; | |
| for (int i = 0; i < history.length; i++) { | |
| final x = (size.width / (history.length + 1)) * (i + 1); | |
| final y = size.height - ((getAvg(history[i]) / 100) * size.height); | |
| points.add(Offset(x, y)); | |
| if (i == 0) path.moveTo(x, y); else path.lineTo(x, y); | |
| } | |
| canvas.drawPath(path, paint); | |
| for (var p in points) { | |
| canvas.drawCircle(p, 6, dotPaint); | |
| } | |
| } | |
| @override | |
| bool shouldRepaint(covariant CustomPainter oldDelegate) => true; | |
| } | |
| /// --- 9. DATA GRID --- | |
| class DataGridView extends StatelessWidget { | |
| final Snapshot snapshot; | |
| const DataGridView(this.snapshot, {super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const _Header("Raw Metrics", "Deep dive audit view."), | |
| Expanded( | |
| child: SingleChildScrollView( | |
| scrollDirection: Axis.vertical, | |
| child: SingleChildScrollView( | |
| scrollDirection: Axis.horizontal, | |
| child: DataTable( | |
| headingRowColor: MaterialStateProperty.all(Colors.white10), | |
| columns: const [ | |
| DataColumn(label: Text("Name")), | |
| DataColumn(label: Text("Total Health")), | |
| DataColumn(label: Text("Mag")), | |
| DataColumn(label: Text("Modular")), | |
| DataColumn(label: Text("Data")), | |
| DataColumn(label: Text("State")), | |
| DataColumn(label: Text("Error")), | |
| DataColumn(label: Text("Reliability")), | |
| ], | |
| rows: snapshot.blocs.map((b) => DataRow(cells: [ | |
| DataCell(Text(b.name, style: const TextStyle(fontWeight: FontWeight.bold))), | |
| DataCell(Text("${b.avgHealth.toInt()}%", style: TextStyle(color: b.healthColor, fontWeight: FontWeight.bold))), | |
| DataCell(Text(b.magnitude.toInt().toString())), | |
| DataCell(Text(b.modularization.toInt().toString())), | |
| DataCell(Text(b.dataIntegrity.toInt().toString())), | |
| DataCell(Text(b.statePattern.toInt().toString())), | |
| DataCell(Text(b.errorHandling.toInt().toString())), | |
| DataCell(Text(b.reliability.toInt().toString())), | |
| ])).toList(), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| /// --- 10. GRADE DISTRIBUTION --- | |
| class DistributionView extends StatelessWidget { | |
| final Snapshot snapshot; | |
| const DistributionView(this.snapshot, {super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| int a = 0, b = 0, c = 0, d = 0, f = 0; | |
| for (var bloc in snapshot.blocs) { | |
| if (bloc.avgHealth >= 90) a++; | |
| else if (bloc.avgHealth >= 75) b++; | |
| else if (bloc.avgHealth >= 60) c++; | |
| else if (bloc.avgHealth >= 40) d++; | |
| else f++; | |
| } | |
| final max = [a,b,c,d,f].reduce(math.max); | |
| Widget bar(String label, int count, Color color) { | |
| return Padding( | |
| padding: const EdgeInsets.symmetric(vertical: 8.0), | |
| child: Row( | |
| children: [ | |
| SizedBox(width: 30, child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18))), | |
| const SizedBox(width: 10), | |
| Expanded( | |
| child: Stack( | |
| children: [ | |
| Container(height: 40, color: Colors.white10), | |
| FractionallySizedBox( | |
| widthFactor: count / (max == 0 ? 1 : max), | |
| child: Container(height: 40, color: color), | |
| ), | |
| Positioned.fill(child: Align(alignment: Alignment.centerLeft, child: Padding( | |
| padding: const EdgeInsets.only(left: 10.0), | |
| child: Text("$count Blocs", style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), | |
| ))) | |
| ], | |
| ), | |
| ) | |
| ], | |
| ), | |
| ); | |
| } | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const _Header("Grade Distribution", "How many Blocs are passing?"), | |
| const SizedBox(height: 20), | |
| bar("A", a, const Color(0xFF00C853)), | |
| bar("B", b, const Color(0xFF64DD17)), | |
| bar("C", c, const Color(0xFFFFD600)), | |
| bar("D", d, const Color(0xFFFF6D00)), | |
| bar("F", f, const Color(0xFFD50000)), | |
| ], | |
| ); | |
| } | |
| } | |
| class _Header extends StatelessWidget { | |
| final String title; | |
| final String subtitle; | |
| const _Header(this.title, this.subtitle); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Padding( | |
| padding: const EdgeInsets.only(bottom: 20.0), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text(title, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.w300)), | |
| Text(subtitle, style: const TextStyle(color: Colors.white54)), | |
| const Divider(height: 30), | |
| ], | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment