mirror of
https://github.com/SyncrowIOT/web.git
synced 2025-07-10 15:17:31 +00:00
1511-occupancy-heat-map-tooltip.
This commit is contained in:
@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:syncrow_web/utils/color_manager.dart';
|
||||||
|
import 'package:syncrow_web/utils/extension/build_context_x.dart';
|
||||||
|
|
||||||
|
class HeatMapTooltip extends StatelessWidget {
|
||||||
|
const HeatMapTooltip({
|
||||||
|
required this.date,
|
||||||
|
required this.value,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DateTime date;
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: AlignmentDirectional.topStart,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: ColorsManager.grey700,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat('MMM d, yyyy').format(date),
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 2, thickness: 1),
|
||||||
|
Text(
|
||||||
|
'$value Occupants',
|
||||||
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: ColorsManager.whiteColors,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart';
|
||||||
|
|
||||||
|
class InteractiveHeatMap extends StatefulWidget {
|
||||||
|
const InteractiveHeatMap({
|
||||||
|
required this.items,
|
||||||
|
required this.maxValue,
|
||||||
|
required this.cellSize,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<OccupancyPaintItem> items;
|
||||||
|
final int maxValue;
|
||||||
|
final double cellSize;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<InteractiveHeatMap> createState() => _InteractiveHeatMapState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InteractiveHeatMapState extends State<InteractiveHeatMap> {
|
||||||
|
OccupancyPaintItem? _hoveredItem;
|
||||||
|
OverlayEntry? _overlayEntry;
|
||||||
|
final LayerLink _layerLink = LayerLink();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_removeOverlay();
|
||||||
|
_overlayEntry?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeOverlay() {
|
||||||
|
_overlayEntry?.remove();
|
||||||
|
_overlayEntry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showTooltip(OccupancyPaintItem item, Offset localPosition) {
|
||||||
|
_removeOverlay();
|
||||||
|
|
||||||
|
final column = item.index ~/ 7;
|
||||||
|
final row = item.index % 7;
|
||||||
|
final x = column * widget.cellSize;
|
||||||
|
final y = row * widget.cellSize;
|
||||||
|
|
||||||
|
_overlayEntry = OverlayEntry(
|
||||||
|
builder: (context) => Positioned(
|
||||||
|
child: CompositedTransformFollower(
|
||||||
|
link: _layerLink,
|
||||||
|
offset: Offset(x + widget.cellSize, y),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Transform.translate(
|
||||||
|
offset: Offset(-(widget.cellSize * 2.5), -50),
|
||||||
|
child: HeatMapTooltip(date: item.date, value: item.value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Overlay.of(context).insert(_overlayEntry!);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CompositedTransformTarget(
|
||||||
|
link: _layerLink,
|
||||||
|
child: MouseRegion(
|
||||||
|
onHover: (event) {
|
||||||
|
final column = event.localPosition.dx ~/ widget.cellSize;
|
||||||
|
final row = event.localPosition.dy ~/ widget.cellSize;
|
||||||
|
final index = column * 7 + row;
|
||||||
|
|
||||||
|
if (index >= 0 && index < widget.items.length) {
|
||||||
|
final item = widget.items[index];
|
||||||
|
if (_hoveredItem != item) {
|
||||||
|
setState(() => _hoveredItem = item);
|
||||||
|
_showTooltip(item, event.localPosition);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_removeOverlay();
|
||||||
|
setState(() => _hoveredItem = null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onExit: (_) {
|
||||||
|
_removeOverlay();
|
||||||
|
setState(() => _hoveredItem = null);
|
||||||
|
},
|
||||||
|
child: CustomPaint(
|
||||||
|
isComplex: true,
|
||||||
|
size: _painterSize,
|
||||||
|
painter: OccupancyPainter(
|
||||||
|
items: widget.items,
|
||||||
|
maxValue: widget.maxValue,
|
||||||
|
hoveredItem: _hoveredItem,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Size get _painterSize {
|
||||||
|
final height = 7 * widget.cellSize;
|
||||||
|
final width = widget.items.length ~/ 7 * widget.cellSize;
|
||||||
|
return Size(width, height);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:math' as math show max;
|
import 'dart:math' as math show max;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart';
|
||||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
|
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart';
|
||||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart';
|
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart';
|
||||||
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart';
|
import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart';
|
||||||
@ -14,9 +15,8 @@ class OccupancyHeatMap extends StatelessWidget {
|
|||||||
static const _cellSize = 16.0;
|
static const _cellSize = 16.0;
|
||||||
static const _totalWeeks = 53;
|
static const _totalWeeks = 53;
|
||||||
|
|
||||||
int get _maxValue => heatMapData.isNotEmpty
|
int get _maxValue =>
|
||||||
? heatMapData.keys.map((key) => heatMapData[key]!).reduce(math.max)
|
heatMapData.isNotEmpty ? heatMapData.values.reduce(math.max) : 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
DateTime _getStartingDate() {
|
DateTime _getStartingDate() {
|
||||||
final jan1 = DateTime(DateTime.now().year, 1, 1);
|
final jan1 = DateTime(DateTime.now().year, 1, 1);
|
||||||
@ -28,7 +28,7 @@ class OccupancyHeatMap extends StatelessWidget {
|
|||||||
return List.generate(_totalWeeks * 7, (index) {
|
return List.generate(_totalWeeks * 7, (index) {
|
||||||
final date = startDate.add(Duration(days: index));
|
final date = startDate.add(Duration(days: index));
|
||||||
final value = heatMapData[date] ?? 0;
|
final value = heatMapData[date] ?? 0;
|
||||||
return OccupancyPaintItem(index: index, value: value);
|
return OccupancyPaintItem(index: index, value: value, date: date);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,15 +58,13 @@ class OccupancyHeatMap extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const OccupancyHeatMapDays(cellSize: _cellSize),
|
const OccupancyHeatMapDays(cellSize: _cellSize),
|
||||||
CustomPaint(
|
SizedBox(
|
||||||
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
|
width: _totalWeeks * _cellSize,
|
||||||
child: CustomPaint(
|
height: 7 * _cellSize,
|
||||||
isComplex: true,
|
child: InteractiveHeatMap(
|
||||||
size: const Size(_totalWeeks * _cellSize, 7 * _cellSize),
|
items: paintItems,
|
||||||
painter: OccupancyPainter(
|
maxValue: _maxValue,
|
||||||
items: paintItems,
|
cellSize: _cellSize,
|
||||||
maxValue: _maxValue,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -4,18 +4,25 @@ import 'package:syncrow_web/utils/color_manager.dart';
|
|||||||
class OccupancyPaintItem {
|
class OccupancyPaintItem {
|
||||||
final int index;
|
final int index;
|
||||||
final int value;
|
final int value;
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
const OccupancyPaintItem({required this.index, required this.value});
|
const OccupancyPaintItem({
|
||||||
|
required this.index,
|
||||||
|
required this.value,
|
||||||
|
required this.date,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class OccupancyPainter extends CustomPainter {
|
class OccupancyPainter extends CustomPainter {
|
||||||
OccupancyPainter({
|
OccupancyPainter({
|
||||||
required this.items,
|
required this.items,
|
||||||
required this.maxValue,
|
required this.maxValue,
|
||||||
|
this.hoveredItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<OccupancyPaintItem> items;
|
final List<OccupancyPaintItem> items;
|
||||||
final int maxValue;
|
final int maxValue;
|
||||||
|
final OccupancyPaintItem? hoveredItem;
|
||||||
|
|
||||||
static const double cellSize = 16.0;
|
static const double cellSize = 16.0;
|
||||||
|
|
||||||
@ -25,6 +32,10 @@ class OccupancyPainter extends CustomPainter {
|
|||||||
final Paint borderPaint = Paint()
|
final Paint borderPaint = Paint()
|
||||||
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
|
..color = ColorsManager.grayBorder.withValues(alpha: 0.4)
|
||||||
..style = PaintingStyle.stroke;
|
..style = PaintingStyle.stroke;
|
||||||
|
final Paint hoveredBorderPaint = Paint()
|
||||||
|
..color = Colors.black
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 1.5;
|
||||||
|
|
||||||
for (final item in items) {
|
for (final item in items) {
|
||||||
final column = item.index ~/ 7;
|
final column = item.index ~/ 7;
|
||||||
@ -37,22 +48,27 @@ class OccupancyPainter extends CustomPainter {
|
|||||||
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
|
final rect = Rect.fromLTWH(x, y, cellSize, cellSize);
|
||||||
canvas.drawRect(rect, fillPaint);
|
canvas.drawRect(rect, fillPaint);
|
||||||
|
|
||||||
_drawDashedLine(
|
// Highlight the hovered item
|
||||||
canvas,
|
if (hoveredItem != null && hoveredItem!.index == item.index) {
|
||||||
Offset(x, y),
|
canvas.drawRect(rect, hoveredBorderPaint);
|
||||||
Offset(x + cellSize, y),
|
} else {
|
||||||
borderPaint,
|
_drawDashedLine(
|
||||||
);
|
canvas,
|
||||||
_drawDashedLine(
|
Offset(x, y),
|
||||||
canvas,
|
Offset(x + cellSize, y),
|
||||||
Offset(x, y + cellSize),
|
borderPaint,
|
||||||
Offset(x + cellSize, y + cellSize),
|
);
|
||||||
borderPaint,
|
_drawDashedLine(
|
||||||
);
|
canvas,
|
||||||
|
Offset(x, y + cellSize),
|
||||||
|
Offset(x + cellSize, y + cellSize),
|
||||||
|
borderPaint,
|
||||||
|
);
|
||||||
|
|
||||||
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
|
canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint);
|
||||||
canvas.drawLine(
|
canvas.drawLine(Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize),
|
||||||
Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize), borderPaint);
|
borderPaint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,5 +96,6 @@ class OccupancyPainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
bool shouldRepaint(covariant OccupancyPainter oldDelegate) =>
|
||||||
|
oldDelegate.hoveredItem != hoveredItem;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user