feat(chart): interactive zoom/pan controls, pattern overlays, predictions, individual pattern filtering and landscape fullscreen mode

This commit is contained in:
2026-08-15 01:03:57 +02:00
parent 1ccb6b613f
commit 5a6a50a609
12 changed files with 1501 additions and 1165 deletions
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/glass_container.dart';
import '../../utils/metric_explanations.dart';
class IndicatorRibbonBar extends StatelessWidget {
final bool showSma50;
final bool showSma200;
final bool showEma;
final bool showSupertrend;
final bool showPatterns;
final bool showSignals;
final ValueChanged<bool> onToggleSma50;
final ValueChanged<bool> onToggleSma200;
final ValueChanged<bool> onToggleEma;
final ValueChanged<bool> onToggleSupertrend;
final ValueChanged<bool> onTogglePatterns;
final ValueChanged<bool> onToggleSignals;
final VoidCallback? onToggleFullscreen;
final bool isFullscreen;
const IndicatorRibbonBar({
super.key,
required this.showSma50,
required this.showSma200,
required this.showEma,
required this.showSupertrend,
required this.showPatterns,
required this.showSignals,
required this.onToggleSma50,
required this.onToggleSma200,
required this.onToggleEma,
required this.onToggleSupertrend,
required this.onTogglePatterns,
required this.onToggleSignals,
this.onToggleFullscreen,
this.isFullscreen = false,
});
Widget _buildChip(BuildContext context, String label, bool isSelected, ValueChanged<bool> onChanged, Color color) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
FilterChip(
selected: isSelected,
label: Text(
label,
style: TextStyle(
color: isSelected ? Colors.black : color,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
selectedColor: color,
backgroundColor: color.withValues(alpha: 0.15),
side: BorderSide(color: color.withValues(alpha: 0.4)),
showCheckmark: false,
onSelected: onChanged,
),
InkWell(
onTap: () => MetricExplanations.show(context, label),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Icon(Icons.info_outline, size: 14, color: AppTheme.textMuted),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return GlassContainer(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildChip(context, 'SMA 50', showSma50, onToggleSma50, AppTheme.accentCyan),
const SizedBox(width: 8),
_buildChip(context, 'SMA 200', showSma200, onToggleSma200, Colors.amber),
const SizedBox(width: 8),
_buildChip(context, 'EMA 20', showEma, onToggleEma, Colors.purpleAccent),
const SizedBox(width: 8),
_buildChip(context, 'Supertrend', showSupertrend, onToggleSupertrend, AppTheme.primaryEmerald),
const SizedBox(width: 8),
_buildChip(context, 'Muster', showPatterns, onTogglePatterns, Colors.orangeAccent),
const SizedBox(width: 8),
_buildChip(context, 'Signale', showSignals, onToggleSignals, Colors.greenAccent),
if (onToggleFullscreen != null) ...[
const SizedBox(width: 12),
Container(width: 1, height: 20, color: AppTheme.activePreset.glassBorder),
const SizedBox(width: 12),
InkWell(
onTap: onToggleFullscreen,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: AppTheme.activePreset.cardSurface,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.activePreset.glassBorder),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
size: 16,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
isFullscreen ? 'Normal' : 'Vollbild',
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
),
),
],
],
),
),
);
}
}
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/glass_container.dart';
import '../../../../core/widgets/status_badge.dart';
import '../../models/technical_analysis_model.dart';
import '../../utils/pattern_explanations.dart';
class PatternCardItem extends StatelessWidget {
final ChartPatternModel pattern;
final int index;
final bool isEnabled;
final ValueChanged<bool> onToggle;
const PatternCardItem({
super.key,
required this.pattern,
required this.index,
required this.isEnabled,
required this.onToggle,
});
@override
Widget build(BuildContext context) {
final patternColor = PatternExplanations.getColorForPattern(pattern.type);
final allPoints = [...pattern.upperLine, ...pattern.lowerLine];
DateTime? startDate;
DateTime? endDate;
if (allPoints.isNotEmpty) {
allPoints.sort((a, b) => a.time.compareTo(b.time));
startDate = allPoints.first.time;
endDate = allPoints.last.time;
}
final dateFormat = DateFormat('dd.MM.yy');
final dateStr = startDate != null && endDate != null
? '${dateFormat.format(startDate)} - ${dateFormat.format(endDate)}'
: 'Unbekannt';
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: GlassContainer(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
children: [
Checkbox(
value: isEnabled,
activeColor: patternColor,
checkColor: Colors.black,
side: BorderSide(color: patternColor.withValues(alpha: 0.6)),
onChanged: (bool? val) => onToggle(val ?? false),
),
Expanded(
child: InkWell(
onTap: () => PatternExplanations.showPatternDetails(context, pattern.type),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isEnabled ? patternColor.withValues(alpha: 0.15) : AppTheme.glassSurface,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.polyline_outlined,
color: isEnabled ? patternColor : AppTheme.textMuted,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
pattern.type,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isEnabled ? Colors.white : AppTheme.textMuted,
fontSize: 14,
decoration: isEnabled ? null : TextDecoration.lineThrough,
),
),
const SizedBox(width: 6),
Icon(Icons.info_outline, size: 14, color: AppTheme.textMuted),
],
),
Text(
'Zeitraum: $dateStr\n'
'Linien: Oben (${pattern.upperLine.length} Pkt.) / Unten (${pattern.lowerLine.length} Pkt.)',
style: TextStyle(color: AppTheme.textMuted, fontSize: 11, height: 1.3),
),
],
),
),
StatusBadge(
label: isEnabled ? 'AKTIV' : 'AUS',
color: isEnabled ? patternColor : AppTheme.textMuted,
),
],
),
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/glass_container.dart';
import '../../../../core/widgets/status_badge.dart';
import '../../models/technical_analysis_model.dart';
class SignalCardItem extends StatelessWidget {
final StrategySignalModel signal;
const SignalCardItem({super.key, required this.signal});
@override
Widget build(BuildContext context) {
final isBuy = signal.type.toUpperCase() == 'BUY';
final color = isBuy ? AppTheme.primaryEmerald : AppTheme.accentRed;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: GlassContainer(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: Icon(isBuy ? Icons.north_east : Icons.south_east, color: color, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
signal.type.toUpperCase(),
style: TextStyle(fontWeight: FontWeight.bold, color: color, fontSize: 14),
),
const SizedBox(width: 8),
Text(
'@ €${signal.price.toStringAsFixed(2)}',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13),
),
],
),
const SizedBox(height: 4),
Text(
'Strategisches Kaufsignal ausgelöst durch technische Indikatoren.',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
),
],
),
),
StatusBadge(label: 'SIGNAL', color: color),
],
),
),
);
}
}