Add tooltips, hover effects, clicks, and panning to your charts
CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue') .geomPoint() .tooltip(DefaultTooltips.simple('revenue')) .build()
CristalyseChart() .data(data) .mapping(x: 'week', y: 'sales', color: 'region') .geomBar() .tooltip(DefaultTooltips.multi({ 'week': 'Week', 'sales': 'Sales ($)', 'region': 'Region', 'growth': 'Growth Rate', })) .build()
CristalyseChart() .data(data) .mapping(x: 'date', y: 'price', color: 'symbol') .geomLine() .tooltip((point) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black.withOpacity(0.9), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.white24), boxShadow: const [ BoxShadow( color: Colors.black26, blurRadius: 12.0, offset: Offset(0, 4), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${point.getDisplayValue('symbol')}', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 4), Text( 'Price: \$${point.getDisplayValue('price')}', style: const TextStyle(color: Colors.white), ), Text( 'Date: ${point.getDisplayValue('date')}', style: const TextStyle(color: Colors.white70, fontSize: 12), ), ], ), ); }) .build()
CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .interaction( tooltip: TooltipConfig( builder: DefaultTooltips.simple('value'), showDelay: const Duration(milliseconds: 100), hideDelay: const Duration(milliseconds: 500), followPointer: true, backgroundColor: const Color(0xFF323232), textColor: Colors.white, borderRadius: 8.0, padding: const EdgeInsets.all(12), shadow: const BoxShadow( color: Colors.black26, blurRadius: 8.0, offset: Offset(0, 2), ), ), ) .build()
CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .geomBar() .onHover((point) { if (point != null) { print('Hovering over: ${point.getDisplayValue('category')}'); } else { print('No longer hovering'); } }) .build()
CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint(size: 8.0) .interaction( hover: HoverConfig( onHover: (point) { // Handle hover start if (point != null) { setState(() { hoveredPoint = point; }); } }, onExit: (point) { // Handle hover end setState(() { hoveredPoint = null; }); }, hitTestRadius: 20.0, // Generous hit area debounce: const Duration(milliseconds: 50), ), ) .build()
CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue') .geomBar() .onClick((point) { Navigator.push( context, MaterialPageRoute( builder: (context) => DetailPage(data: point.data), ), ); }) .build()
CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint() .interaction( click: ClickConfig( onTap: (point) { showBottomSheet( context: context, builder: (context) => DataPointDetails(point: point), ); }, onDoubleTap: (point) { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Edit Data Point'), content: EditDataPointForm(point: point), ), ); }, onLongPress: (point) { Clipboard.setData(ClipboardData( text: 'Value: ${point.getDisplayValue('y')}', )); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Copied to clipboard')), ); }, hitTestRadius: 15.0, ), ) .build()
CristalyseChart() .data(largeTimeSeriesData) .mapping(x: 'timestamp', y: 'value') .geomLine() .onPan((info) { print('Visible range: ${info.visibleMinX} to ${info.visibleMaxX}'); // Update data source or trigger lazy loading if (info.state == PanState.end) { fetchDataForRange(info.visibleMinX, info.visibleMaxX); } }) .build()
CristalyseChart() .data(data) .mapping(x: 'time', y: 'metric') .geomLine() .geomPoint() .interaction( pan: PanConfig( enabled: true, updateXDomain: true, // Enable X-axis panning updateYDomain: false, // Disable Y-axis panning onPanStart: (info) { setState(() { isPanning = true; }); }, onPanUpdate: (info) { setState(() { visibleRange = '${info.visibleMinX?.toStringAsFixed(1)} - ${info.visibleMaxX?.toStringAsFixed(1)}'; }); }, onPanEnd: (info) { setState(() { isPanning = false; }); // Fetch new data for visible range loadDataForVisibleRange(info.visibleMinX, info.visibleMaxX); }, throttle: const Duration(milliseconds: 16), // 60 FPS ), ) .build()
class PanningChart extends StatefulWidget { @override _PanningChartState createState() => _PanningChartState(); } class _PanningChartState extends State<PanningChart> { List<Map<String, dynamic>> visibleData = []; double currentMinX = 0; double currentMaxX = 100; @override void initState() { super.initState(); loadDataForRange(currentMinX, currentMaxX); } void loadDataForRange(double? minX, double? maxX) async { if (minX == null || maxX == null) return; // Simulate API call final newData = await fetchTimeSeriesData(minX, maxX); setState(() { visibleData = newData; currentMinX = minX; currentMaxX = maxX; }); } @override Widget build(BuildContext context) { return CristalyseChart() .data(visibleData) .mapping(x: 'timestamp', y: 'value', color: 'series') .geomLine(strokeWidth: 2.0) .scaleXContinuous(min: currentMinX, max: currentMaxX) .interaction( pan: PanConfig( enabled: true, updateXDomain: true, onPanEnd: (info) => loadDataForRange( info.visibleMinX, info.visibleMaxX, ), ), tooltip: TooltipConfig( builder: DefaultTooltips.multi({ 'timestamp': 'Time', 'value': 'Value', 'series': 'Series', }), ), ) .build(); } }
CristalyseChart() .data(data) .mapping(x: 'date', y: 'price', color: 'symbol', size: 'volume') .geomPoint(alpha: 0.8) .interaction( tooltip: TooltipConfig( builder: (point) => RichTooltip( title: point.getDisplayValue('symbol'), fields: { 'price': 'Price', 'volume': 'Volume', 'change': 'Change %', }, point: point, ), showDelay: const Duration(milliseconds: 50), hideDelay: const Duration(milliseconds: 300), ), hover: HoverConfig( onHover: (point) => highlightRelatedPoints(point), onExit: (point) => clearHighlights(), hitTestRadius: 12.0, ), click: ClickConfig( onTap: (point) => showStockDetails(point), onDoubleTap: (point) => addToWatchlist(point), ), pan: PanConfig( enabled: true, updateXDomain: true, onPanUpdate: (info) => updateVisibleTimeRange(info), ), ) .build()
CristalyseChart() .data(largeDataset) // 10,000+ points .mapping(x: 'x', y: 'y', color: 'category') .geomPoint(size: 3.0, alpha: 0.7) .interaction( tooltip: TooltipConfig( builder: DefaultTooltips.simple('y'), showDelay: const Duration(milliseconds: 10), // Fast response ), hover: HoverConfig( hitTestRadius: 8.0, // Smaller hit area for performance debounce: const Duration(milliseconds: 16), // 60 FPS ), pan: PanConfig( enabled: true, throttle: const Duration(milliseconds: 32), // 30 FPS for panning ), ) .build()
class PerformantTooltips { static final _tooltipPool = <Widget>[]; static Widget pooled(DataPointInfo point) { // Reuse existing tooltip widgets when possible return Text('Value: ${point.getDisplayValue('value')}'); } } CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .tooltip(PerformantTooltips.pooled) .build()
CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue') .geomBar() .interaction( tooltip: TooltipConfig( builder: (point) => Semantics( label: 'Revenue for ${point.getDisplayValue('month')}: \$${point.getDisplayValue('revenue')}k', child: DefaultTooltips.simple('revenue')(point), ), ), click: ClickConfig( onTap: (point) { // Announce selection to screen readers SemanticsService.announce( 'Selected ${point.getDisplayValue('month')} with revenue \$${point.getDisplayValue('revenue')}k', TextDirection.ltr, ); }, ), ) .build()
class KeyboardNavigableChart extends StatefulWidget { @override _KeyboardNavigableChartState createState() => _KeyboardNavigableChartState(); } class _KeyboardNavigableChartState extends State<KeyboardNavigableChart> { int? selectedIndex; @override Widget build(BuildContext context) { return Focus( onKey: (node, event) { if (event is RawKeyDownEvent) { if (event.logicalKey == LogicalKeyboardKey.arrowRight) { setState(() { selectedIndex = (selectedIndex ?? -1) + 1; selectedIndex = selectedIndex!.clamp(0, data.length - 1); }); return KeyEventResult.handled; } // Handle other keys... } return KeyEventResult.ignored; }, child: CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .build(), ); } }
Hit Areas
Performance
User Experience
Accessibility