# Custom Themes Source: https://docs.cristalyse.com/advanced/custom-themes Create and apply custom themes for personalized chart aesthetics. ## Overview Custom themes in Cristalyse offer complete flexibility to express your brand's unique style. Design themes with specific colors, typography, spacing, and other visual elements to ensure consistency across all charts. ## Designing a Custom Theme ### Theme Properties Customize various properties: * **Colors**: Define primary, secondary, background, and accent colors. * **Typography**: Adjust fonts, sizes, and weights. * **Dimensions**: Set padding, margins, and element sizes. ### Creating Themes Design a theme matching your brand: ```dart theme={null} final customTheme = ChartTheme( backgroundColor: const Color(0xFFF0F0F0), plotBackgroundColor: Colors.white, primaryColor: const Color(0xFF0050AC), // Brand primary borderColor: const Color(0xFFCCCCCC), gridColor: const Color(0xFFE0E0E0), axisColor: const Color(0xFF333333), gridWidth: 1.0, axisWidth: 1.5, pointSizeDefault: 6.0, colorPalette: [ const Color(0xFF0050AC), // Primary const Color(0xFFFF9500), // Accent const Color(0xFF5A2EA6), // Secondary const Color(0xFF34C759), // Success ], padding: const EdgeInsets.all(20), axisTextStyle: const TextStyle( fontSize: 12, color: Color(0xFF333333), fontWeight: FontWeight.w400, ), ); ``` ## Applying Themes Apply themes in charts: ```dart theme={null} CristalyseChart() .data(chartData) .mapping(x: 'month', y: 'visitors') .geomBar() .theme(customTheme) .build() ``` ## Theme Variants ### Light and Dark Modes Create matching themes for light and dark modes: ```dart theme={null} final lightTheme = ChartTheme.defaultTheme(); final darkTheme = ChartTheme.darkTheme(); CristalyseChart() .data(data) .mapping(x: 'category', y: 'count') .geomBar() .theme(isDarkMode ? darkTheme : lightTheme) .build(); ``` ### Responsive Themes Adjust theme properties based on screen size: ```dart theme={null} ChartTheme responsiveTheme(BuildContext context) { final width = MediaQuery.of(context).size.width; if (width > 1200) { // Desktop return ChartTheme.defaultTheme().copyWith( padding: const EdgeInsets.all(32), axisTextStyle: const TextStyle(fontSize: 14), ); } else if (width > 800) { // Tablet return ChartTheme.defaultTheme().copyWith( padding: const EdgeInsets.all(24), ); } else { // Mobile return ChartTheme.defaultTheme().copyWith( padding: const EdgeInsets.all(16), axisTextStyle: const TextStyle(fontSize: 10), ); } } ``` ## Gradient Themes (Experimental) **New Experimental Feature in v1.6.0**: Create stunning gradient effects for your charts with category-specific gradient mapping. **Not advisable for Production use as of v1.6.0.** ### Creating Gradient Themes Define gradients for specific categories in your data: ```dart theme={null} // Create gradients map for use with .customPalette() final productGradients = { // Linear gradients for different product tiers 'Enterprise': const LinearGradient( colors: [Color(0xFF8B5CF6), Color(0xFFA78BFA)], begin: Alignment.bottomLeft, end: Alignment.topRight, stops: [0.0, 1.0], ), 'Professional': const LinearGradient( colors: [Color(0xFF3B82F6), Color(0xFF60A5FA)], begin: Alignment.bottomCenter, end: Alignment.topCenter, ), // Radial gradient for special categories 'Premium': const RadialGradient( colors: [Color(0xFFDC2626), Color(0xFFEF4444), Color(0xFFFCA5A5)], center: Alignment.center, radius: 1.2, stops: [0.0, 0.7, 1.0], ), // Sweep gradient for dynamic effects 'Special': const SweepGradient( colors: [ Color(0xFFEAB308), Color(0xFFF59E0B), Color(0xFFDC2626), Color(0xFFEAB308), ], center: Alignment.center, startAngle: 0.0, endAngle: 2 * math.pi, ), }; ``` ### Gradient Types #### Linear Gradients Smooth color transitions along a line: ```dart theme={null} const LinearGradient( colors: [Color(0xFF4F46E5), Color(0xFF7C3AED)], begin: Alignment.bottomCenter, // Start point end: Alignment.topCenter, // End point stops: [0.0, 1.0], // Optional color stops ) ``` #### Radial Gradients Circular color transitions from center outward: ```dart theme={null} const RadialGradient( colors: [Color(0xFF059669), Color(0xFF10B981)], center: Alignment.center, // Center point radius: 0.8, // Radius (0.0 to 1.0+) focal: Alignment.center, // Optional focal point focalRadius: 0.1, // Optional focal radius ) ``` #### Sweep Gradients Angular color transitions around a center point: ```dart theme={null} const SweepGradient( colors: [Color(0xFFEAB308), Color(0xFFF59E0B), Color(0xFFEAB308)], center: Alignment.center, // Center point startAngle: 0.0, // Start angle in radians endAngle: 2 * math.pi, // End angle in radians ) ``` ### Applying Gradient Themes Use gradient themes with color mapping: ```dart theme={null} CristalyseChart() .data(productData) .mapping(x: 'month', y: 'sales', color: 'tier') // Color mapping required .geomBar() .customPalette(categoryGradients: productGradients) .build() ``` **Important**: Gradients only work when you specify color mapping in your chart. Without `color: 'columnName'`, charts will use solid colors from the color palette. ### Supported Chart Types Gradient themes currently support: * **Bar Charts** - Full gradient support with alpha blending * **Point/Scatter Charts** - Gradient fills for data points * **Line Charts** - Not yet supported (solid colors only) * **Area Charts** - Not yet supported (solid colors only) * **Bubble Charts** - Not yet supported (solid colors only) ## Advanced Customization ### Theme Extensions Add additional properties using extensions: ```dart theme={null} extension ChartThemeExtras on ChartTheme { ChartTheme get withCustomColors => copyWith( primaryColor: Colors.teal, colorPalette: [Colors.teal, Colors.amber, Colors.purple], ); } final extendedTheme = ChartTheme.defaultTheme().withCustomColors; ``` ### Dynamic Themes Change themes in response to user actions and preferences: ```dart theme={null} class ThemeSwitcher extends StatefulWidget { final ChartTheme lightTheme; final ChartTheme darkTheme; final List> data; const ThemeSwitcher({ required this.lightTheme, required this.darkTheme, required this.data, super.key, }); @override _ThemeSwitcherState createState() => _ThemeSwitcherState(); } class _ThemeSwitcherState extends State { bool isDark = false; @override Widget build(BuildContext context) { return Column( children: [ Switch( value: isDark, onChanged: (value) { setState(() { isDark = value; }); }, ), Expanded( child: CristalyseChart() .data(widget.data) .mapping(x: 'time', y: 'value') .geomLine() .theme(isDark ? widget.darkTheme : widget.lightTheme) .build(), ), ], ); } } ``` ## Example Gallery Match your organization's color scheme and brand identity. Automatic adjustments for different device sizes and resolutions. Save and restore user preferences for consistent experiences. Real-time adjustments and theme switching based on user actions. ## Next Steps Enhance theme application with performance optimizations. Export charts with customized themes for visual consistency. Explore data mapping techniques for thematic accuracy. Combine custom themes with dynamic animations for engaging visuals. ## Conclusion Utilize custom themes to ensure charts are a seamless part of your application's visual identity. By leveraging Cristalyse's flexible theming capabilities, create charts that not only convey data but also resonate with your brand's aesthetics. # Data Mapping Source: https://docs.cristalyse.com/advanced/data-mapping Map data fields to visual properties for flexible visualization ## Overview Data mapping in Cristalyse allows you to flexibly bind data columns to visual chart properties like axis, color, and size, making it easy to represent complex datasets meaningfully. ## Basic Mapping ### Mapping Axes Bind data fields to the X and Y axes: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'date', y: 'temperature') .geomLine() .build() ``` ### Multi-Field Mapping Visualize multiple metrics simultaneously: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue', size: 'profit', color: 'region') .geomPoint() .build() ``` ## Advanced Data Mapping ### Dynamic Mapping Change mappings at runtime to reflect dynamic data changes: ```dart theme={null} CristalyseChart() .data(data) .mapping( x: 'week', y: 'growth', color: isRevenue ? 'revenue' : 'profit', ) .geomLine() .build() ``` ## Best Practices ### Consistent Scales * Maintain consistency in axis mapping across related charts. * Utilize color mapping for quick insights in comparison charts. ### Real-Time Mapping * Allow dynamic mapping for real-time data presentations. * Prepare data transformations before passing to chart mapping. ## Combining Mappings Combine color, size, and shape mappings for detailed insights: ```dart theme={null} CristalyseChart() .data(data) .mapping( x: 'hour', y: 'traffic', size: 'conversions', color: 'channel', ) .geomPoint() .build() ``` ## Example Gallery Map sales data to both revenue and profit margins. Dynamic mapping for fluctuating environmental data. Visualize customer segments using size and color mapping. Use time mapping to align tasks with deadlines. ## Next Steps Master scale transformations for enhanced data storytelling. Enhance user interaction through responsive mapping. Style mappings to fit branding needs. Optimize mapping for scalable application. # Performance Source: https://docs.cristalyse.com/advanced/performance Optimize charting for high performance and responsiveness ## Overview Cristalyse is designed to handle complex charts with large datasets efficiently. Performance optimization ensures smooth rendering, quick interactions, and responsive updates. ## Rendering Performance ### Efficient Data Handling Reduce data points for visualization without losing insights: ```dart theme={null} List> downsampleData(List> data, int targetCount) { final step = data.length / targetCount; return List.generate(targetCount, (i) => data[(i * step).floor()]); } CristalyseChart() .data(downsampleData(originalData, 1000)) .mapping(x: 'x', y: 'y') .geomLine() .build() ``` ### Asynchronous Operations Load data and build charts asynchronously: ```dart theme={null} Future loadDataAndBuild() async { final data = await fetchDataAsync(); setState(() { CristalyseChart() .data(data) .mapping(x: 'time', y: 'value') .geomLine() .build(); }); } ``` ### Memory Management Use efficient data structures and dispose of unnecessary elements: ```dart theme={null} class EfficientChart extends StatefulWidget { @override _EfficientChartState createState() => _EfficientChartState(); } class _EfficientChartState extends State with AutomaticKeepAliveClientMixin { List>? cachedData; @override void dispose() { cachedData = null; // Release memory super.dispose(); } @override Widget build(BuildContext context) { super.build(context); return CristalyseChart() .data(cachedData ?? [] ) .mapping(x: 'x', y: 'value') .geomBar() .build(); } @override bool get wantKeepAlive => true; } ``` ## Interactive Performance ### Debouncing Interactions Control frequency of interaction events for better performance: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .interaction( hover: HoverConfig( debounce: const Duration(milliseconds: 50), onHover: (point) => { if (point != null) print('Hovered ${point.getDisplayValue('x')}') }, ), ) .build() ``` ### Throttling Panning Smooth pan interactions with controlled update frequency: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'time', y: 'metric') .geomLine() .interaction( pan: PanConfig( enabled: true, throttle: const Duration(milliseconds: 32), // 30 FPS onPanUpdate: (info) => handleUpdate(info.visibleMinX, info.visibleMaxX), ), ) .build() ``` ## UI Performance ### Lazily Build Complex Widgets Render parts of your UI only when needed: ```dart theme={null} List buildWidgets() { return List.generate(100, (index) => LazyWidget(data: data[index])); } ListView( children: buildWidgets(), ) ``` ### Render Performance Insights Monitor and optimize paint times: ```dart theme={null} class RenderProfiler extends StatefulWidget { @override _RenderProfilerState createState() => _RenderProfilerState(); } class _RenderProfilerState extends State { DateTime? _lastPaint; @override Widget build(BuildContext context) { return RepaintBoundary( child: CristalyseChart() .data(data) .mapping(x: 'time', y: 'value') .geomArea() .build(), onRepaint: () { final now = DateTime.now(); if (_lastPaint != null) { print('Time since last repaint: ${now.difference(_lastPaint!).inMilliseconds}ms'); } _lastPaint = now; }, ); } } ``` ## Performance Considerations ### Key Considerations * Use `async/await` for data loading to prevent UI blocking. * Downsample large datasets to improve responsiveness. * Profile interaction and rendering performance using profiling tools. ## Performance Techniques ### Combined Techniques Integrate various performance methods to maximize efficiency: ```dart theme={null} CristalyseChart() .data(downsampleData(originalData, 2000)) .mapping(x: 'timestamp', y: 'value') .geomLine() .interaction( tooltip: TooltipConfig( builder: (point) => Text('${point.getDisplayValue('value')}'), showDelay: Duration.zero, // Instantaneous ), pan: PanConfig( enabled: true, throttle: Duration(milliseconds: 16), ), ) .build() ``` ### Animation Performance Handle animations efficiently for better visual fluidity: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomLine() .animate( duration: const Duration(milliseconds: 1500), curve: Curves.easeInOut, ) .interaction( hover: HoverConfig( debounce: const Duration(milliseconds: 10), ), ) .build() ``` ## Example Gallery Efficiently handle continuous real-time data streams. Develop fluid, responsive dashboards with fast interactions. Tackle interdependent charts with shared data sources. Achieve smooth performance with minimal computing resources. ## Next Steps Master performance techniques related to scaling algorithms. Optimize chart exporting for better performance on large datasets. Combine performance optimization with elegant animations. Efficiently map data fields to maximize responsiveness. ## Advanced Tools Utilize Flutter's build-in performance and rendering tools: * **DevTools**: An advanced suite to debug and analyze UI performance. * **Flame Graphs**: Visualize rendering performance and identify bottlenecks. * **Repaint Rainbow**: Find unnecessary paint calls and optimize rendering. Explore these tools to gain deeper insights into app performance and enhance the efficiency of your charts. By mastering these tools and techniques, you ensure your Cristalyse visualizations remain engaging, reliable, and performant even as complexity increases. Explore these tools to increase your knowledge and efficiency when building high-performance visualizations with Cristalyse. # Scales Source: https://docs.cristalyse.com/advanced/scales Comprehensive guide on scales in Cristalyse ## Overview Scales in Cristalyse transform data values to visual positions or dimensions on a chart. They handle both continuous and categorical data, ensuring intuitive and precise data representation. ## Linear Scales Linear scales map continuous data to a range, such as pixels: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'time', y: 'value') .scaleXContinuous(min: 0, max: 100) // X-axis with fixed visual range .scaleYContinuous(min: 0, max: 200) // Y-axis with fixed visual range .geomLine() .build() ``` ### Axis Titles **New in v1.10.0:** Add descriptive titles to your axes for better data context! All scale methods now accept an optional `title` parameter to display descriptive axis labels: ```dart theme={null} CristalyseChart() .data(temperatureData) .mapping(x: 'hour', y: 'temperature') .scaleXContinuous(title: 'Time (hours)') .scaleYContinuous(title: 'Temperature (°C)') .geomLine() .build() ``` **Title Features:** * **Smart Positioning**: Automatically positioned with optimal spacing * **Rotated Text**: Y-axis titles rotate -90°, Y2-axis titles rotate +90° * **Theme-Aware**: Inherits text style from theme with slightly larger font * **Optional**: Only renders when provided (fully backward compatible) **Dual-Axis Example:** ```dart theme={null} CristalyseChart() .data(businessData) .mapping(x: 'month', y: 'revenue') .mappingY2('conversion') .geomBar(yAxis: YAxis.primary) .geomLine(yAxis: YAxis.secondary) .scaleXOrdinal(title: 'Month') .scaleYContinuous(title: 'Revenue ($K)') // Left axis .scaleY2Continuous(title: 'Conversion Rate (%)') // Right axis .build() ``` ### Configuration * **Domain**: Data range mapped to chart space. * **Range**: Pixel space where the data is plotted. * **Limits**: Optional `min`/`max` parameters to constrain the displayed scale range. * **Title**: Optional descriptive label for the axis (v1.10.0+) * **Invertible**: Values can map back to the data space. ### Scale Limits and Data Filtering Use `min` and `max` parameters to control scale behavior: ```dart theme={null} // Data-driven scaling (default behavior) CristalyseChart() .data(temperatureData) // Values: [15.2, 22.8, 19.5, 16.1] .mapping(x: 'hour', y: 'temp') .scaleYContinuous() // Scale automatically fits data range .geomLine() .build() // Fixed range scaling with limits CristalyseChart() .data(temperatureData) .mapping(x: 'hour', y: 'temp') .scaleYContinuous(min: 10, max: 30) // Fixed 10-30°C range .geomLine() .build() ``` Use limits for consistent ranges across multiple charts and to set explicit meaningful baselines (like 0 for revenue). Without limits, the behavior varies by geometry. Geometries that are value comparisons (Bar Charts, Area Charts) have zero baseline by default. For other geometries, the scale domain is fit to the actual data range (15.2-22.8°C). With limits, the scale domain uses the specified range (10-30°C), including overriding a zero baseline for e.g. Bar Charts and Area Charts. Data points beyond the limits still render proportionally beyond the visual range. ## Ordinal Scales Ordinal scales map categorical data to discrete locations: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .scaleXOrdinal() // X-axis for categories .geomBar() .build() ``` **With Axis Title:** ```dart theme={null} CristalyseChart() .data(salesData) .mapping(x: 'product', y: 'revenue') .scaleXOrdinal(title: 'Product Category') .scaleYContinuous(title: 'Revenue ($K)') .geomBar() .build() ``` ### Configuration * **Domain**: List of categories. * **Range**: Space for categories to be displayed. * **BandWidth**: Space allocated to each category. * **Title**: Optional descriptive label for the axis (v1.10.0+) ## Color Scales Color scales are used to encode data using color gradients: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'intensity') .geomPoint() .theme( ChartTheme.defaultTheme().copyWith( colorPalette: [Colors.blue, Colors.red], // Gradient ), ) .build() ``` ## Size Scales Size scales map data values to sizes, useful for scatter plots: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', size: 'value') .geomPoint() .build() ``` ## Label Formatting Transform axis labels by passing a callback to the `labels` parameter on any axis scale that takes any number and returns a string. ### Direct NumberFormat Usage For simple cases, you can use `NumberFormat` from Dart's `intl` package: ```dart theme={null} import 'package:intl/intl.dart'; // Revenue chart with currency formatting CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue') .scaleYContinuous(labels: NumberFormat.simpleCurrency().format) // $1,234.56 .build() // Conversion rate chart with percentage formatting CristalyseChart() .data(conversionData) .mapping(x: 'month', y: 'rate') .scaleYContinuous(labels: NumberFormat.percentPattern().format) // 23% .build() // User growth chart with compact formatting CristalyseChart() .data(userGrowthData) .mapping(x: 'date', y: 'users') .scaleYContinuous(labels: NumberFormat.compact().format) // 1.2K, 1.5M .build() ``` ### Custom Callbacks for Advanced Cases When you need custom logic beyond NumberFormat, use a factory pattern to create a callback based on that logic. ```dart theme={null} // Create formatter once based on passed locale, reuse callback static String Function(num) createCurrencyFormatter({String locale = 'en_US'}) { final formatter = NumberFormat.simpleCurrency(locale: locale); // Created once return (num value) => formatter.format(value); // Reused callback } // Usage final currencyLabels = createCurrencyFormatter(); // uses default here, but could internationalize // Revenue chart with currency formatting CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue') .scaleYContinuous(labels: currencyLabels) // pass callback .build() ``` #### Conditional Formatting (Value-Based Logic) ```dart theme={null} // Time duration formatting (seconds to human readable) static String Function(num) createDurationFormatter() { return (num seconds) { final roundedSeconds = seconds.round(); // Round to nearest second first if (roundedSeconds >= 3600) { final hours = roundedSeconds / 3600; if (hours == hours.round()) { return '${hours.round()}h'; // Clean: "1h", "2h", "24h" } return '${hours.toStringAsFixed(1)}h'; // Decimal: "1.5h", "2.3h" } else if (roundedSeconds >= 60) { final minutes = (roundedSeconds / 60).round(); return '${minutes}m'; // "1m", "30m", "59m" } return '${roundedSeconds}s'; // "5s", "30s", "59s" }; } final usageLabels = createDurationFormatter(); CristalyseChart() .data(usageData) .mapping(x: 'day', y: 'usage') .scaleYContinuous(labels: usageLabels) // pass callback .build() ``` #### Chart-Optimized Formatting (Clean Integers) ```dart theme={null} // Implement chart-friendly integer/decimal distinction w/ NumberFormat static String Function(num) createChartCurrencyFormatter() { final formatter = NumberFormat.simpleCurrency(locale: 'en_US'); return (num value) { if (value == value.roundToDouble()) { return formatter.format(value).replaceAll('.00', ''); // $42 } return formatter.format(value); // $42.50 }; } final currencyLabels = createChartCurrencyFormatter(); CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue') .scaleYContinuous(labels: currencyLabels) .build() ``` #### Business Logic Formatting ```dart theme={null} // Handle negative values differently (P&L charts) static String Function(num) createProfitLossFormatter() { final formatter = NumberFormat.compact(); // Created once return (num value) { final abs = value.abs(); final formatted = formatter.format(abs); // Reuse formatter return value >= 0 ? formatted : '($formatted)'; }; } final currencyLabels = createProfitLossFormatter(); CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue') .scaleYContinuous(labels: currencyLabels) .build() // Custom units (basis points for finance - converts decimal rates to bp) static String Function(num) createBasisPointFormatter() { return (num value) => '${(value * 10000).toStringAsFixed(0)}bp'; } final bpLabels = createBasisPointFormatter(); CristalyseChart() .data(yieldData) // Data has decimal rates like 0.0025, 0.0150 .mapping(x: 'maturity', y: 'yield_rate') // yield_rate is decimal (0.0025 = 25bp) .scaleYContinuous(labels: bpLabels) // Converts 0.0025 → "25bp" .build() ``` ## Tick Configuration **New in v1.14.0:** Control how tick marks and labels are generated on your axes! TickConfig allows you to customize tick generation on continuous scales: ```dart theme={null} TickConfig( ticks: [0, 25, 50, 75, 100], // Optional: specify explicit tick positions simpleLinear: true, // Optional: use simple linear ticks instead of Wilkinson's algorithm ) ``` ### Explicit Tick Specification Provide your own tick positions for precise control: ```dart theme={null} CristalyseChart() .data(temperatureData) .mapping(x: 'hour', y: 'temp') .scaleYContinuous( tickConfig: TickConfig( ticks: [-10, 0, 10, 20, 30], // Custom tick positions ), ) .geomLine() .build() ``` **Use cases:** * Industry-standard reference points (e.g., freezing point at 0°C) * Domain-specific intervals (e.g., percentiles at 25, 50, 75) * Matching ticks across multiple related charts ### Simple Linear Ticking Generate evenly-spaced ticks instead of using Wilkinson's algorithm: ```dart theme={null} CristalyseChart() .data(timeSeriesData) .mapping(x: 'timestamp', y: 'value') .scaleXContinuous( tickConfig: TickConfig(simpleLinear: true), ) .scaleYContinuous( tickConfig: TickConfig(simpleLinear: true), ) .geomLine() .build() ``` **Differences:** * **Wilkinson's Algorithm** (default): Intelligently chooses "nice" numbers for human-readable labels. Tick intervals vary based on data range (e.g., 10, 25, 50, 100). * **Simple Linear**: Always generates evenly-spaced ticks. Tick interval is constant based on the scale range divided by number of ticks. **When to use Simple Linear:** * Time-based charts where consistent intervals matter (e.g., hourly or daily splits) * Scientific/technical charts where precision is preferred over "niceness" * When you need predictable, uniform spacing ### Dual-Axis Example ```dart theme={null} CristalyseChart() .data(businessData) .mapping(x: 'month', y: 'revenue') .mappingY2('conversion') .scaleXOrdinal() .scaleYContinuous( tickConfig: TickConfig(ticks: [0, 50000, 100000, 150000]), ) .scaleY2Continuous( tickConfig: TickConfig(simpleLinear: true), ) .geomBar(yAxis: YAxis.primary) .geomLine(yAxis: YAxis.secondary) .build() ``` ## Scale Customization Customize scales to fit your data representation needs: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'day', y: 'hours') .scaleXOrdinal() .scaleYContinuous(min: 0, max: 24, labels: (v) => '${v}h') // Custom hour labels .build() ``` ## Best Practices ### Scaling Data * Choose linear scales for time-series and numerical data. * Use ordinal scales for categorical data like labels or segments. * Opt for consistent scale units across charts. ### Performance * Ensure domain and range values are correctly configured. * Minimize dynamic scale recalculation for better performance. * Use scale inversions in interactive applications for better feedback. ## Advanced Usage ### Dual Axis Charts Implement dual axes for complex comparisons: ```dart theme={null} CristalyseChart() .data(dualAxisData) .mapping(x: 'month', y: 'revenue') .mappingY2('conversion_rate') .scaleYContinuous(min: 0, max: 100) .scaleY2Continuous(min: 0, max: 1) .geomBar(yAxis: YAxis.primary) .geomLine(yAxis: YAxis.secondary) .build() ``` ### Conditional Scales Dynamically adjust scales based on data: ```dart theme={null} CristalyseChart() .data(dynamicData) .mapping(x: 'date', y: 'value') .scaleXContinuous(min: startDate, max: endDate) .scaleYContinuous(min: minValue, max: maxValue) .geomArea() .build() ``` ## Example Gallery Vivid depiction of temporal trends and changes over time. Clear comparison of discrete categories using ordinal scaling. Dual-axis chart illustrating financial performance metrics. Interactive scales adapting to real-time data changes. ## Next Steps Effective data field-to-visual property mapping techniques. Create custom themes to match brand identity. Optimize visual performance with tailored scaling strategies. Engage users with interactive scaled charts. ## Technical Details Review the source code behind scale transformations and extend as needed within the `scale.dart` file. # Area Charts Source: https://docs.cristalyse.com/charts/area-charts Filled area charts for showing volume and trends over time See area charts in action with interactive examples ## Overview Area charts are great for displaying volume and cumulative data over time. Cristalyse adds smooth animations and options for multi-series, stacked, and combined area charts. ## Basic Area Chart Show volume over time with a filled area: ```dart theme={null} final data = [ {'month': 'Jan', 'visitors': 1200}, {'month': 'Feb', 'visitors': 1350}, {'month': 'Mar', 'visitors': 1100}, ]; CristalyseChart() .data(data) .mapping(x: 'month', y: 'visitors') .geomArea( strokeWidth: 2.0, alpha: 0.3, color: Colors.blue, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .animate(duration: Duration(milliseconds: 1200)) .build() ``` ## Multi-Series Area Chart Compare multiple categories with stacked areas: ```dart theme={null} final platformData = [ {'month': 'Jan', 'users': 800, 'platform': 'Mobile'}, {'month': 'Feb', 'users': 900, 'platform': 'Mobile'}, {'month': 'Jan', 'users': 400, 'platform': 'Desktop'}, {'month': 'Feb', 'users': 450, 'platform': 'Desktop'}, ]; CristalyseChart() .data(platformData) .mapping(x: 'month', y: 'users', color: 'platform') .geomArea(strokeWidth: 1.5, alpha: 0.4) .scaleXOrdinal() .scaleYContinuous(min: 0) .build() ``` ## Combined Area + Line + Points Layered visualization for deep insights: ```dart theme={null} CristalyseChart() .data(analyticsData) .mapping(x: 'week', y: 'engagement') .geomArea(alpha: 0.2, strokeWidth: 0) // Background fill .geomLine() .geomPoint(size: 5.0) .theme(ChartTheme.darkTheme()) .build() ``` ## Stacked Area Charts Visualize composition of total data: ```dart theme={null} final revenueData = [ {'quarter': 'Q1', 'revenue': 80, 'category': 'Software'}, {'quarter': 'Q1', 'revenue': 40, 'category': 'Hardware'}, {'quarter': 'Q2', 'revenue': 100, 'category': 'Software'}, {'quarter': 'Q2', 'revenue': 60, 'category': 'Hardware'}, ]; CristalyseChart() .data(revenueData) .mapping(x: 'quarter', y: 'revenue', color: 'category') .geomArea(strokeWidth: 2.0, alpha: 0.3) .scaleXOrdinal() .scaleYContinuous(min: 0) .build() ``` ## Interactive Features ### Hover Tooltips Provide context with tooltips: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'visitors') .geomArea( alpha: 0.3, strokeWidth: 2.0, ) .interaction( tooltip: TooltipConfig( builder: (point) = Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.8), borderRadius: BorderRadius.circular(4), ), child: Text( '${point.getDisplayValue('month')}: ${point.getDisplayValue('visitors')} visitors', style: TextStyle(color: Colors.white), ), ), ), ) .build() ``` ## Performance Optimizations Consider these tips for optimal performance with large datasets: * Use lighter gradients and colors * Optimize data point count and reduce chart complexity ## Common Uses Ideal for displaying trends in data like website traffic, sales, or user engagement. Area charts excel in visualizing relative compositions or whole-part relationships. ## Best Practices * Use transparency wisely to avoid overly dark fills * Pick colors that align with your brand identity * Ensure color contrast for legibility * Avoid overcrowding data points * Consider time-based intervals for X-axis * Focus on key data points for emphasis * Use tooltips to provide detailed insights * Consider adding data labels for clarity Ready to dive deeper into advanced visualizations? [Explore Dual Axis Charts](charts/dual-axis) # Bar Charts Source: https://docs.cristalyse.com/charts/bar-charts Categorical data visualization with multiple variations See bar charts in action with interactive examples ## Overview Bar charts are ideal for comparing categorical data. Cristalyse supports vertical, horizontal, grouped, and stacked bar charts with smooth animations and customizable styling.
Animated Bar Chart Horizontal Bar Chart
Vertical and horizontal bar charts with staggered animations
## Basic Bar Chart Create a simple vertical bar chart: ```dart theme={null} final data = [ {'quarter': 'Q1', 'revenue': 120}, {'quarter': 'Q2', 'revenue': 150}, {'quarter': 'Q3', 'revenue': 110}, {'quarter': 'Q4', 'revenue': 180}, ]; CristalyseChart() .data(data) .mapping(x: 'quarter', y: 'revenue') .geomBar( width: 0.8, alpha: 0.9, borderRadius: BorderRadius.circular(4), ) .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .build() ``` ## Grouped Bar Charts Compare multiple series side-by-side:
Grouped Bar Chart
Grouped bar charts for comparing multiple series side-by-side
```dart theme={null} final productData = [ {'quarter': 'Q1', 'revenue': 120, 'product': 'Widget A'}, {'quarter': 'Q2', 'revenue': 150, 'product': 'Widget A'}, {'quarter': 'Q1', 'revenue': 80, 'product': 'Widget B'}, {'quarter': 'Q2', 'revenue': 110, 'product': 'Widget B'}, ]; CristalyseChart() .data(productData) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar( style: BarStyle.grouped, width: 0.8, alpha: 0.9, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .legend() // ✨ Auto-generates legend for each product .build() ``` ## Stacked Bar Charts Show composition and totals:
Stacked Bar Chart
Stacked bars with segment-by-segment progressive animation
```dart theme={null} final budgetData = [ {'department': 'Marketing', 'amount': 50, 'category': 'Personnel'}, {'department': 'Marketing', 'amount': 30, 'category': 'Technology'}, {'department': 'Marketing', 'amount': 20, 'category': 'Travel'}, {'department': 'Sales', 'amount': 80, 'category': 'Personnel'}, {'department': 'Sales', 'amount': 25, 'category': 'Technology'}, ]; CristalyseChart() .data(budgetData) .mapping(x: 'department', y: 'amount', color: 'category') .geomBar( style: BarStyle.stacked, width: 0.8, alpha: 0.9, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .legend(position: LegendPosition.right) // ✨ Legend showing budget categories .build() ``` **New in v1.5.0**: Legends automatically generate from your color mapping. Perfect for grouped and stacked bar charts! ## Horizontal Bar Charts Perfect for long category names or ranking data: ```dart theme={null} final departmentData = [ {'department': 'Engineering', 'headcount': 45}, {'department': 'Product', 'headcount': 25}, {'department': 'Sales', 'headcount': 35}, {'department': 'Marketing', 'headcount': 20}, {'department': 'Customer Success', 'headcount': 15}, ]; CristalyseChart() .data(departmentData) .mapping(x: 'department', y: 'headcount') .geomBar( borderRadius: BorderRadius.circular(4), borderWidth: 1.0, ) .coordFlip() // Makes it horizontal .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .build() ``` ## Styling Options ### Rounded Corners Add modern styling with border radius: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .geomBar( borderRadius: BorderRadius.circular(8), alpha: 0.8, ) .build() ``` ### Borders Add definition with borders: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .geomBar( borderWidth: 2.0, alpha: 0.7, ) .build() ``` ### Custom Colors Override default color mapping: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .geomBar( color: Colors.deepPurple, alpha: 0.8, ) .build() ``` ### Positive/Negative Value Styling **New in v1.17.0**: Style positive and negative bars differently with smart rounded corners and conditional colors. Perfect for financial data, variance charts, and profit/loss visualizations. #### Smart Rounded Corners Round only the outward edges of bars (away from the zero baseline): ```dart theme={null} final varianceData = [ {'category': 'A', 'value': 25.0}, {'category': 'B', 'value': -15.0}, {'category': 'C', 'value': 40.0}, {'category': 'D', 'value': -30.0}, ]; CristalyseChart() .data(varianceData) .mapping(x: 'category', y: 'value') .geomBar( borderRadius: BorderRadius.circular(12), roundOutwardEdges: true, // Positive: rounded top, Negative: rounded bottom ) .scaleXOrdinal() .scaleYContinuous() .build() ``` #### Conditional Colors Apply different colors based on value sign: ```dart theme={null} CristalyseChart() .data(varianceData) .mapping(x: 'category', y: 'value') .geomBar( borderRadius: BorderRadius.circular(12), roundOutwardEdges: true, positiveColor: Colors.green, // Green for gains/profits negativeColor: Colors.red, // Red for losses/deficits ) .scaleXOrdinal() .scaleYContinuous() .build() ``` #### Combined Example (Financial Dashboard) ```dart theme={null} final profitLossData = [ {'month': 'Jan', 'pnl': 45000.0}, {'month': 'Feb', 'pnl': -12000.0}, {'month': 'Mar', 'pnl': 67000.0}, {'month': 'Apr', 'pnl': -8000.0}, {'month': 'May', 'pnl': 89000.0}, ]; CristalyseChart() .data(profitLossData) .mapping(x: 'month', y: 'pnl') .geomBar( width: 0.7, borderRadius: BorderRadius.circular(8), roundOutwardEdges: true, positiveColor: const Color(0xFF10B981), // Emerald green negativeColor: const Color(0xFFEF4444), // Red ) .scaleXOrdinal() .scaleYContinuous( labels: (value) => '\$${(value / 1000).toStringAsFixed(0)}k', ) .theme(ChartTheme.defaultTheme()) .animate( duration: Duration(milliseconds: 1000), curve: Curves.easeOutBack, ) .build() ``` **Pro Tip**: Combine `roundOutwardEdges` with `positiveColor`/`negativeColor` for professional-looking financial charts. The sharp edge at the zero baseline creates a clean, aligned appearance. ### Gradient Colors **New in v1.6.0**: Add stunning gradient effects to your bar charts! Support for linear, radial, and sweep gradients with full alpha blending. Create beautiful gradient bars using category-specific gradients: ```dart theme={null} final data = [ {'quarter': 'Q1', 'revenue': 120}, {'quarter': 'Q2', 'revenue': 150}, {'quarter': 'Q3', 'revenue': 110}, {'quarter': 'Q4', 'revenue': 180}, ]; CristalyseChart() .data(data) .mapping(x: 'quarter', y: 'revenue', color: 'quarter') .geomBar( width: 0.8, alpha: 0.9, borderRadius: BorderRadius.circular(8), ) .scaleXOrdinal() .scaleYContinuous(min: 0) .customPalette(categoryGradients: { 'Q1': const LinearGradient( colors: [Color(0xFF4F46E5), Color(0xFF7C3AED)], begin: Alignment.bottomCenter, end: Alignment.topCenter, ), 'Q2': const LinearGradient( colors: [Color(0xFF059669), Color(0xFF10B981)], begin: Alignment.bottomCenter, end: Alignment.topCenter, ), 'Q3': const RadialGradient( colors: [Color(0xFFDC2626), Color(0xFFEF4444)], center: Alignment.center, radius: 0.8, ), 'Q4': const SweepGradient( colors: [Color(0xFFEAB308), Color(0xFFF59E0B), Color(0xFFEAB308)], center: Alignment.center, ), }) .build() ``` #### Advanced Gradient Examples Multiple gradient types for regional sales data: ```dart theme={null} final salesData = [ {'region': 'North', 'sales': 250, 'product': 'Premium'}, {'region': 'South', 'sales': 180, 'product': 'Standard'}, {'region': 'East', 'sales': 320, 'product': 'Premium'}, {'region': 'West', 'sales': 210, 'product': 'Standard'}, ]; CristalyseChart() .data(salesData) .mapping(x: 'region', y: 'sales', color: 'product') .geomBar( style: BarStyle.grouped, width: 0.8, borderRadius: BorderRadius.circular(6), ) .scaleXOrdinal() .scaleYContinuous(min: 0) .customPalette(categoryGradients: { 'Premium': const LinearGradient( colors: [Color(0xFF8B5CF6), Color(0xFFA78BFA)], stops: [0.0, 1.0], ), 'Standard': const LinearGradient( colors: [Color(0xFF3B82F6), Color(0xFF60A5FA)], stops: [0.0, 1.0], ), }) .legend() .build() ``` #### Gradient Features * **Linear Gradients**: Perfect for vertical or horizontal color transitions * **Radial Gradients**: Create circular color effects from center outward * **Sweep Gradients**: Circular gradients that sweep around a center point * **Alpha Blending**: Gradients automatically respect animation alpha values * **Type Safety**: Strongly typed gradient maps for reliability **Important**: Gradients are only applied when using color mapping (i.e., when you specify `color: 'columnName'` in your mapping). Without color mapping, bars will use solid colors from the theme's color palette. ## Animation Types ### Progressive Bar Growth Bars animate from bottom to top: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .geomBar() .animate( duration: Duration(milliseconds: 1200), curve: Curves.easeInOutCubic, ) .build() ``` ### Staggered Animation Each bar animates with a slight delay: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .geomBar() .animate( duration: Duration(milliseconds: 1400), curve: Curves.elasticOut, ) .build() ``` ## Dual Y-Axis Bar Charts Combine bars with different scales: ```dart theme={null} final mixedData = [ {'quarter': 'Q1', 'revenue': 120, 'efficiency': 85}, {'quarter': 'Q2', 'revenue': 150, 'efficiency': 92}, {'quarter': 'Q3', 'revenue': 110, 'efficiency': 78}, ]; CristalyseChart() .data(mixedData) .mapping(x: 'quarter', y: 'revenue') .mappingY2('efficiency') .geomBar(yAxis: YAxis.primary) // Revenue bars .geomLine( yAxis: YAxis.secondary, // Efficiency line strokeWidth: 3.0, color: Colors.orange, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .scaleY2Continuous(min: 0, max: 100) .build() ``` ## Interactive Bar Charts ### Hover Effects Add rich tooltips on hover: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'quarter', y: 'revenue') .geomBar() .interaction( tooltip: TooltipConfig( builder: (point) { return Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.8), borderRadius: BorderRadius.circular(4), ), child: Text( '${point.getDisplayValue('quarter')}: \$${point.getDisplayValue('revenue')}k', style: TextStyle(color: Colors.white), ), ); }, ), ) .build() ``` ### Click Handlers React to bar selection: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'quarter', y: 'revenue') .geomBar() .interaction( click: ClickConfig( onTap: (point) { print('Clicked: ${point.data}'); // Navigate to detail view showQuarterDetails(point.data); }, ), ) .build() ``` ## Best Practices * Use width 0.6-0.8 for optimal readability * Avoid very thin bars (\< 0.3) or very thick bars (> 0.9) * Grouped bars automatically adjust width * Use consistent color palettes * Consider colorblind-friendly schemes * Limit categories to 8-10 colors maximum * Ensure data values are positive * Order categories consistently * Consider total height for readability * Limit to 50-100 bars for optimal performance * Use simpler borders for large datasets * Consider data aggregation for very large sets ## Common Use Cases Compare performance across regions or time periods Show spending breakdown with stacked bars Display categorical survey responses Rank teams or products by metrics ## Next Steps Visualize volume and cumulative data Combine different metrics on independent scales Add smooth transitions and effects Make charts interactive with tooltips and events # Bubble Charts Source: https://docs.cristalyse.com/charts/bubble-charts Three-dimensional data visualization with position and size encoding See bubble charts in action with interactive examples ## Overview Bubble charts are powerful scatter plots where the size of each point (bubble) represents a third dimension of data. They excel at visualizing relationships between three continuous variables simultaneously, making them perfect for market analysis, performance dashboards, and multi-dimensional data exploration. ## Basic Bubble Chart Create a simple bubble chart with three data dimensions: ```dart theme={null} final marketData = [ {'revenue': 120.0, 'customers': 150.0, 'marketShare': 15.0, 'name': 'TechCorp'}, {'revenue': 200.0, 'customers': 120.0, 'marketShare': 25.0, 'name': 'FinanceInc'}, {'revenue': 80.0, 'customers': 200.0, 'marketShare': 10.0, 'name': 'HealthPlus'}, {'revenue': 150.0, 'customers': 220.0, 'marketShare': 30.0, 'name': 'RetailMax'}, ]; CristalyseChart() .data(marketData) .mapping( x: 'revenue', // X position y: 'customers', // Y position size: 'marketShare' // Bubble size ) .geomBubble() .scaleXContinuous() .scaleYContinuous() .build() ``` ## Color Mapping Add categorical grouping with color encoding: ```dart theme={null} final companyData = [ {'revenue': 120.0, 'customers': 150.0, 'marketShare': 15.0, 'category': 'Enterprise', 'name': 'TechFlow Solutions'}, {'revenue': 80.0, 'customers': 200.0, 'marketShare': 10.0, 'category': 'SMB', 'name': 'DataSync Pro'}, {'revenue': 60.0, 'customers': 180.0, 'marketShare': 8.0, 'category': 'Startup', 'name': 'InnovateLab'}, {'revenue': 200.0, 'customers': 120.0, 'marketShare': 25.0, 'category': 'Enterprise', 'name': 'GlobalTech Industries'}, ]; CristalyseChart() .data(companyData) .mapping( x: 'revenue', y: 'customers', size: 'marketShare', color: 'category' // Color by category ) .geomBubble( alpha: 0.7, // Semi-transparent for overlaps borderWidth: 2.0, // Border for better definition ) .scaleXContinuous(min: 0) .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .build() ``` ## Bubble Sizing ### Size Range Control Control the minimum and maximum bubble sizes for typical data: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'revenue', y: 'customers', size: 'marketShare') .geomBubble( minSize: 8.0, // Minimum radius in pixels maxSize: 25.0, // Maximum radius in pixels alpha: 0.75, ) .build() ``` ### Size Guide in Legend **New in v1.10.0:** Add a visual size guide to your legend showing min, mid, and max bubble sizes! Provide a `title` parameter to `geomBubble()` to automatically display a bubble size guide in the legend: ```dart theme={null} CristalyseChart() .data(companyData) .mapping(x: 'revenue', y: 'customers', size: 'marketShare', color: 'category') .geomBubble( title: 'Market Share (%)', // Enables size guide in legend minSize: 8.0, maxSize: 25.0, alpha: 0.75, borderWidth: 2.0, ) .legend() // Shows both category colors AND bubble size guide .build() ``` **Size Guide Features:** * **Visual Reference**: Shows three bubbles (min, mid, max) with actual data values * **Automatic Scaling**: Values and sizes match your data and scale configuration * **Layout Aware**: Adapts to horizontal or vertical legend positioning * **Integrated**: Works seamlessly with existing legend items and color keys **Example with Custom Legend Position:** ```dart theme={null} CristalyseChart() .data(marketData) .mapping( x: 'revenue', y: 'customers', size: 'marketShare', color: 'category', ) .geomBubble( title: 'Market Share (%)', minSize: 10.0, maxSize: 30.0, alpha: 0.8, ) .legend( position: LegendPosition.right, // Size guide on right side interactive: true, // Enable click-to-toggle ) .build() ``` ### Advanced Bubble Sizing For precise control over the scale domain and visual presentation, use both `limits` and `minSize`/`maxSize`: ```dart theme={null} CristalyseChart() .data(salesData) .mapping(x: 'revenue', y: 'profit', size: 'dealSize', color: 'region') .geomBubble( minSize: 10.0, // Visual: minimum bubble radius in pixels maxSize: 40.0, // Visual: maximum bubble radius in pixels limits: (50000, 1000000), // Scale domain: $50K maps to 10px, $1M maps to 40px alpha: 0.8, borderWidth: 1.5, ) .build() ``` In this example: * `minSize`/`maxSize` control how bubbles appear visually (10-40 pixel radius) * `limits` set the scale's reference range: deals at $50K get 10px radius, deals at $1M get 40px radius * **All deals still render** - deals outside limits are scaled proportionally (e.g., \$2M deal gets 80px radius) * Deals within the limits range will be scaled linearly to the visual size range **Important: Outlier Behavior** * **Without `limits`**: Scale domain uses actual data range, so `minSize`/`maxSize` map to actual min/max data values * **With `limits`**: Scale domain is set to limits; values outside limits still render, scaled proportionally beyond minSize/maxSize * This preserves data accuracy - all values render, with outliers appearing larger/smaller than the reference range * Use `limits` to set a consistent scale domain across multiple charts, not to filter data ### Dynamic Size Scaling Use a slider or control to adjust bubble sizes dynamically: ```dart theme={null} class BubbleChartWidget extends StatefulWidget { @override _BubbleChartWidgetState createState() => _BubbleChartWidgetState(); } class _BubbleChartWidgetState extends State { double _sizeMultiplier = 0.5; @override Widget build(BuildContext context) { // Calculate sizes based on multiplier final minSize = 5.0 + _sizeMultiplier * 3.0; // 5-8px final maxSize = 15.0 + _sizeMultiplier * 10.0; // 15-25px return Column( children: [ Slider( value: _sizeMultiplier, min: 0.0, max: 1.0, onChanged: (value) => setState(() => _sizeMultiplier = value), label: 'Bubble Size', ), Expanded( child: CristalyseChart() .data(data) .mapping(x: 'revenue', y: 'customers', size: 'marketShare') .geomBubble(minSize: minSize, maxSize: maxSize) .build(), ), ], ); } } ``` ## Bubble Styling ### Shape Options Customize bubble shapes: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', size: 'size') .geomBubble( shape: PointShape.circle, // circle, square, triangle borderWidth: 2.0, borderColor: Colors.white, alpha: 0.8, ) .build() ``` Available shapes: * `PointShape.circle` (default) * `PointShape.square` * `PointShape.triangle` ### Advanced Styling ```dart theme={null} CristalyseChart() .data(marketData) .mapping(x: 'revenue', y: 'customers', size: 'marketShare', color: 'category') .geomBubble( minSize: 10.0, maxSize: 30.0, alpha: 0.75, // Transparency for overlapping borderWidth: 2.0, // Border thickness borderColor: Colors.white, // Border color shape: PointShape.circle, // Bubble shape ) .build() ``` ## Labels and Text ### Bubble Labels Add labels to bubbles: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'revenue', y: 'customers', size: 'marketShare') .geomBubble( showLabels: true, labelFormatter: (value) => '${value.toStringAsFixed(1)}%', labelStyle: const TextStyle( fontSize: 11, fontWeight: FontWeight.bold, color: Colors.white, ), labelOffset: 0.0, // Center labels on bubbles ) .build() ``` ### Custom Label Positioning ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'revenue', y: 'customers', size: 'marketShare') .geomBubble( showLabels: true, labelOffset: 15.0, // Offset labels from bubble center labelFormatter: (value) => 'Share: ${value}%', labelStyle: TextStyle( fontSize: 10, color: Colors.black87, backgroundColor: Colors.white.withOpacity(0.8), ), ) .build() ``` ## Scale Customization ### Axis Formatting Format axis labels for better readability: ```dart theme={null} CristalyseChart() .data(marketData) .mapping(x: 'revenue', y: 'customers', size: 'marketShare') .geomBubble() .scaleXContinuous( min: 0, labels: (value) => '\$${value.toStringAsFixed(0)}M', // Revenue in millions ) .scaleYContinuous( min: 0, labels: (value) => '${value.toStringAsFixed(0)}K', // Customers in thousands ) .build() ``` ### Size Scale Optimization Ensure meaningful size differences: ```dart theme={null} // Good: Clear size hierarchy .geomBubble(minSize: 8.0, maxSize: 24.0) // 3:1 ratio // Avoid: Too extreme ratios .geomBubble(minSize: 2.0, maxSize: 50.0) // 25:1 ratio - hard to perceive ``` ## Interactive Features ### Rich Tooltips Create informative hover tooltips: ```dart theme={null} CristalyseChart() .data(companyData) .mapping(x: 'revenue', y: 'customers', size: 'marketShare', color: 'category') .geomBubble() .interaction( tooltip: TooltipConfig( builder: (point) { final name = point.getDisplayValue('name'); final revenue = point.getDisplayValue('revenue'); final customers = point.getDisplayValue('customers'); final marketShare = point.getDisplayValue('marketShare'); final category = point.getDisplayValue('category'); return Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.grey[900]!, Colors.grey[800]!], ), borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 12, offset: Offset(0, 6), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold, ), ), SizedBox(height: 8), _buildTooltipRow(Icons.attach_money, 'Revenue', '\$${revenue}M'), _buildTooltipRow(Icons.people, 'Customers', '${customers}K'), _buildTooltipRow(Icons.pie_chart, 'Market Share', '${marketShare}%'), _buildTooltipRow(Icons.category, 'Category', category), ], ), ); }, ), ) .build() Widget _buildTooltipRow(IconData icon, String label, String value) { return Padding( padding: EdgeInsets.symmetric(vertical: 2), child: Row( children: [ Icon(icon, size: 14, color: Colors.grey[400]), SizedBox(width: 6), Text(label, style: TextStyle(color: Colors.grey[400], fontSize: 11)), Spacer(), Text(value, style: TextStyle(color: Colors.white, fontSize: 12)), ], ), ); } ``` ### Click Interactions Handle bubble selection: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'revenue', y: 'customers', size: 'marketShare', color: 'category') .geomBubble() .interaction( click: ClickConfig( onTap: (point) { final companyName = point.getDisplayValue('name'); print('Selected company: $companyName'); // Navigate to company details Navigator.push( context, MaterialPageRoute( builder: (context) => CompanyDetailScreen(company: companyName), ), ); }, ), ) .build() ``` ## Animation ### Entrance Animation Animate bubbles appearing: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'revenue', y: 'customers', size: 'marketShare', color: 'category') .geomBubble(alpha: 0.75) .animate( duration: Duration(milliseconds: 1200), curve: Curves.easeOutCubic, ) .build() ``` ### Smooth Animation Customize animation timing and curves: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'revenue', y: 'customers', size: 'marketShare') .geomBubble() .animate( duration: Duration(milliseconds: 1500), curve: Curves.elasticOut, ) .build() ``` ## Dual Y-Axis Support Use bubble charts with secondary Y-axis: ```dart theme={null} final performanceData = [ {'quarter': 'Q1', 'revenue': 120, 'efficiency': 85, 'satisfaction': 4.2}, {'quarter': 'Q2', 'revenue': 150, 'efficiency': 92, 'satisfaction': 4.5}, {'quarter': 'Q3', 'revenue': 110, 'efficiency': 78, 'satisfaction': 3.9}, ]; CristalyseChart() .data(performanceData) .mapping(x: 'quarter', y: 'revenue', size: 'efficiency') .mappingY2('satisfaction') .geomBar(yAxis: YAxis.primary) // Revenue bars on primary axis .geomBubble( // Satisfaction bubbles on secondary axis yAxis: YAxis.secondary, minSize: 8.0, maxSize: 20.0, color: Colors.orange, alpha: 0.8, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .scaleY2Continuous(min: 1, max: 5) .build() ``` ## Market Analysis Example ### Complete Dashboard A comprehensive market analysis bubble chart: ```dart theme={null} class MarketAnalysisDashboard extends StatefulWidget { @override _MarketAnalysisDashboardState createState() => _MarketAnalysisDashboardState(); } class _MarketAnalysisDashboardState extends State { double _bubbleScale = 1.0; final _marketData = [ { 'name': 'TechCorp Solutions', 'revenue': 250.0, 'customers': 180.0, 'marketShare': 28.0, 'category': 'Enterprise', }, { 'name': 'StartupX Labs', 'revenue': 85.0, 'customers': 120.0, 'marketShare': 12.0, 'category': 'Startup', }, { 'name': 'MidSize Systems', 'revenue': 150.0, 'customers': 160.0, 'marketShare': 18.0, 'category': 'SMB', }, // ... more data ]; @override Widget build(BuildContext context) { final minSize = 8.0 + _bubbleScale * 5.0; final maxSize = 15.0 + _bubbleScale * 15.0; return Scaffold( body: Column( children: [ // Header with controls Container( padding: EdgeInsets.all(16), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Market Performance Analysis', style: Theme.of(context).textTheme.headlineSmall, ), Text('Revenue vs Customer Base'), ], ), ), // Bubble size control Column( children: [ Text('Bubble Size'), Slider( value: _bubbleScale, min: 0.1, max: 2.0, divisions: 19, label: '${(_bubbleScale * 100).round()}%', onChanged: (value) => setState(() => _bubbleScale = value), ), ], ), ], ), ), // Chart Expanded( child: Padding( padding: EdgeInsets.all(16), child: CristalyseChart() .data(_marketData) .mapping( x: 'revenue', y: 'customers', size: 'marketShare', color: 'category', ) .geomBubble( minSize: minSize, maxSize: maxSize, alpha: 0.75, borderWidth: 2.0, borderColor: Colors.white, ) .scaleXContinuous( labels: (value) => '\$${value.toStringAsFixed(0)}M', ) .scaleYContinuous( labels: (value) => '${value.toStringAsFixed(0)}K', ) .theme(ChartTheme.defaultTheme()) .animate( duration: Duration(milliseconds: 1000), curve: Curves.easeOutCubic, ) .interaction( tooltip: TooltipConfig( builder: _buildRichTooltip, ), ) .build(), ), ), // Legend and insights Container( padding: EdgeInsets.all(16), child: Column( children: [ // Category legend _buildLegend(), SizedBox(height: 16), // Key insights _buildInsightsPanel(), ], ), ), ], ), ); } Widget _buildRichTooltip(DataPointInfo point) { final name = point.getDisplayValue('name'); final revenue = point.getDisplayValue('revenue'); final customers = point.getDisplayValue('customers'); final marketShare = point.getDisplayValue('marketShare'); final category = point.getDisplayValue('category'); return Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(8), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), SizedBox(height: 4), Text('Revenue: \$${revenue}M', style: TextStyle(color: Colors.white)), Text('Customers: ${customers}K', style: TextStyle(color: Colors.white)), Text('Market Share: ${marketShare}%', style: TextStyle(color: Colors.white)), Text('Category: ${category}', style: TextStyle(color: Colors.white)), ], ), ); } Widget _buildLegend() { final categories = ['Enterprise', 'SMB', 'Startup']; final colors = [Colors.blue, Colors.green, Colors.orange]; return Row( mainAxisAlignment: MainAxisAlignment.center, children: categories.asMap().entries.map((entry) { return Padding( padding: EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( color: colors[entry.key], shape: BoxShape.circle, ), ), SizedBox(width: 6), Text(entry.value), ], ), ); }).toList(), ); } Widget _buildInsightsPanel() { return Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue[50], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue[200]!), ), child: Column( children: [ Row( children: [ Icon(Icons.insights, color: Colors.blue[700]), SizedBox(width: 8), Text( 'Key Insights', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.blue[900], ), ), ], ), SizedBox(height: 8), Text( '• Bubble size represents market share percentage\n' '• Hover over bubbles for detailed company metrics\n' '• Colors indicate company categories', style: TextStyle(fontSize: 12, color: Colors.blue[700]), ), ], ), ); } } ``` ## Best Practices * Use bubble size for positive, quantitative variables * Keep size ratios between 2:1 and 5:1 for optimal perception * Ensure smallest bubbles are clearly visible (minimum 5-8px radius) * Consider area vs radius scaling based on your data distribution * Use alpha (transparency) for overlapping bubbles * Limit color categories to 6-8 for clarity * Add white borders to improve bubble separation * Consider colorblind-friendly palettes * For dense data, increase transparency and reduce size * Use tooltips instead of labels for cluttered charts * Consider data aggregation or filtering for very large datasets * Provide zoom/pan functionality for detailed exploration * Always include informative tooltips * Use consistent hover states * Consider click-through navigation to detail views * Provide size controls for user customization * For 500+ bubbles, reduce border width or disable borders * Use lower alpha values for better performance * Consider data virtualization for very large datasets * Test on different device sizes and performance levels ## Common Use Cases Revenue vs customers with market share sizing Risk vs return with position size bubbles Multi-dimensional KPI dashboards Three-variable correlation analysis Population vs GDP with area sizing Price vs quality with popularity sizing ## Next Steps Basic two-dimensional point charts Matrix-based data visualization Add tooltips and click handlers Smooth entrance and transition effects # Dual Axis Charts Source: https://docs.cristalyse.com/charts/dual-axis Combine different metrics on independent scales for enhanced data analysis See dual axis charts in action with interactive examples ## Overview Dual axis charts allow you to correlate two different metrics using independent Y-scales. Cristalyse offers full support for dual axis configurations across various chart types, providing a complete view of complex data relationships.
Dual Axis Chart
Dual axis charts for correlating two different metrics on independent scales
## Basic Dual Axis Chart Combine bars and lines with dual Y-axes: ```dart theme={null} final data = [ {'month': 'Jan', 'revenue': 100, 'growth': 2.5}, {'month': 'Feb', 'revenue': 150, 'growth': 3.0}, {'month': 'Mar', 'revenue': 130, 'growth': 2.8}, ]; CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue') // Primary Y-axis .mappingY2('growth') // Secondary Y-axis .geomBar(yAxis: YAxis.primary) // Bind bars to primary axis .geomLine( yAxis: YAxis.secondary, strokeWidth: 2.0, color: Colors.orange, ) .scaleXOrdinal() .scaleYContinuous(min: 0, max: 200) .scaleY2Continuous(min: 0, max: 5) .build() ``` ## Stacked Dual Axis Example Utilize dual axes with stacked bars: ```dart theme={null} final energyData = [ {'year': '2020', 'coal': 30, 'renewables': 20, 'efficiency': 0.8}, {'year': '2021', 'coal': 25, 'renewables': 30, 'efficiency': 0.9}, {'year': '2022', 'coal': 20, 'renewables': 35, 'efficiency': 1.0}, ]; CristalyseChart() .data(energyData) .mapping(x: 'year', y: 'coal') // Primary Y-axis .mappingY2('efficiency') // Secondary Y-axis .geomBar(yAxis: YAxis.primary, // Bind to primary axis color: Colors.blue, alpha: 0.7, style: BarStyle.stacked ) .geomLine( yAxis: YAxis.secondary, strokeWidth: 3.0, color: Colors.green, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .scaleY2Continuous(min: 0, max: 2) .build() ``` ## Interactive Features ### Hover and Zoom Add interactivity to enhance data exploration: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue') .mappingY2('growth') .geomBar(yAxis: YAxis.primary) .geomLine( yAxis: YAxis.secondary, strokeWidth: 2.0, color: Colors.orange, ) .interaction( tooltip: TooltipConfig( builder: (point) { return Text( '${point.getDisplayValue('month')}: Revenue ${point.getDisplayValue('revenue')}k, Growth ${point.getDisplayValue('growth')}%', style: TextStyle(color: Colors.white), ); }, ), ) .build() ``` ## Styling Options Customize colors, scales, and styles to match your needs. Dual axis charts provide flexibility in how data series are presented, whether using bars, lines, or points across primary and secondary axes. ## Best Practices * Choose complementary data series for dual axis charts * Ensure the scales' units are relevant and helpful * Keep the design clear with distinct colors * Avoid information overload - focus on key metrics * Limit the number of points displayed for clarity * Use staggered animations for smoother rendering ## Common Use Cases Correlate revenue and profit margins for comprehensive insights Track sales growth vs conversion rates using multiple axes Compare fuel types and efficiency improvements over time Investigate relationships between distinct datasets ## Ready to explore more? Bring charts to life with smooth transitions Customize chart appearances for brand consistency # Heat Map Charts Source: https://docs.cristalyse.com/charts/heat-map-charts Visualize 2D data patterns with color-coded intensity heat maps See heat map charts in action with interactive examples ## Overview Heat maps are perfect for visualizing 2D data patterns where color intensity represents value magnitude. Cristalyse heat maps support customizable color schemes (via `colorGradient`), interactive tooltips, and responsive layouts for effective data exploration. **API Note**: Heat maps use `.mappingHeatMap()` instead of `.mapping()`, and color gradients are specified via the `colorGradient` parameter in `.geomHeatMap()`. If no `colorGradient` is specified, the default `GradientColorScale.heatMap()` gradient (dark blue → cyan → lime green → bright red) is automatically applied.
Heat maps with customizable color gradients and interactive cell highlighting
## Basic Heat Map Create a simple heat map to show data density: ```dart theme={null} final performanceData = [ {'region': 'North', 'month': 'Jan', 'sales': 120}, {'region': 'North', 'month': 'Feb', 'sales': 135}, {'region': 'North', 'month': 'Mar', 'sales': 98}, {'region': 'South', 'month': 'Jan', 'sales': 85}, {'region': 'South', 'month': 'Feb', 'sales': 92}, {'region': 'South', 'month': 'Mar', 'sales': 110}, {'region': 'West', 'month': 'Jan', 'sales': 165}, {'region': 'West', 'month': 'Feb', 'sales': 140}, {'region': 'West', 'month': 'Mar', 'sales': 180}, ]; CristalyseChart() .data(performanceData) .mappingHeatMap(x: 'month', y: 'region', value: 'sales') .geomHeatMap( cellSpacing: 2.0, cellBorderRadius: BorderRadius.circular(4), showValues: true, // Specify custom gradient or omit for default GradientColorScale.heatMap() colorGradient: [Colors.lightBlue.shade100, Colors.blue.shade800], interpolateColors: true, ) .scaleXOrdinal() .scaleYOrdinal() .theme(ChartTheme.defaultTheme()) .build() ``` ## Business Performance Heat Map Monitor regional sales performance across months: ```dart theme={null} final businessData = [ {'region': 'North America', 'month': 'Q1', 'sales': 450}, {'region': 'North America', 'month': 'Q2', 'sales': 520}, {'region': 'North America', 'month': 'Q3', 'sales': 380}, {'region': 'North America', 'month': 'Q4', 'sales': 620}, {'region': 'Europe', 'month': 'Q1', 'sales': 320}, {'region': 'Europe', 'month': 'Q2', 'sales': 290}, {'region': 'Europe', 'month': 'Q3', 'sales': 410}, {'region': 'Europe', 'month': 'Q4', 'sales': 480}, {'region': 'Asia Pacific', 'month': 'Q1', 'sales': 180}, {'region': 'Asia Pacific', 'month': 'Q2', 'sales': 220}, {'region': 'Asia Pacific', 'month': 'Q3', 'sales': 280}, {'region': 'Asia Pacific', 'month': 'Q4', 'sales': 350}, ]; CristalyseChart() .data(businessData) .mappingHeatMap(x: 'month', y: 'region', value: 'sales') .geomHeatMap( cellSpacing: 2.0, cellBorderRadius: BorderRadius.circular(6), showValues: true, colorGradient: [Colors.red.shade200, Colors.orange, Colors.green.shade600], interpolateColors: true, valueFormatter: (value) => '\$${value.toStringAsFixed(0)}K', valueTextStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ), ) .scaleXOrdinal() .scaleYOrdinal() .theme(ChartTheme.defaultTheme()) .build() ``` ## System Monitoring Heat Map Visualize server response times across hours and services: ```dart theme={null} final monitoringData = [ {'hour': '00:00', 'service': 'API Gateway', 'responseTime': 45}, {'hour': '00:00', 'service': 'Auth Service', 'responseTime': 32}, {'hour': '00:00', 'service': 'Database', 'responseTime': 28}, {'hour': '06:00', 'service': 'API Gateway', 'responseTime': 120}, {'hour': '06:00', 'service': 'Auth Service', 'responseTime': 85}, {'hour': '06:00', 'service': 'Database', 'responseTime': 95}, {'hour': '12:00', 'service': 'API Gateway', 'responseTime': 200}, {'hour': '12:00', 'service': 'Auth Service', 'responseTime': 150}, {'hour': '12:00', 'service': 'Database', 'responseTime': 180}, {'hour': '18:00', 'service': 'API Gateway', 'responseTime': 180}, {'hour': '18:00', 'service': 'Auth Service', 'responseTime': 130}, {'hour': '18:00', 'service': 'Database', 'responseTime': 160}, ]; CristalyseChart() .data(monitoringData) .mappingHeatMap(x: 'hour', y: 'service', value: 'responseTime') .geomHeatMap( cellSpacing: 1.0, cellBorderRadius: BorderRadius.circular(2), showValues: true, minValue: 0, maxValue: 250, colorGradient: [ Colors.green.shade400, Colors.yellow, Colors.red.shade600, ], interpolateColors: true, nullValueColor: Colors.grey.shade300, valueFormatter: (value) => '${value.toInt()}ms', valueTextStyle: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: Colors.white, ), ) .scaleXOrdinal() .scaleYOrdinal() .theme(ChartTheme.darkTheme()) .build() ``` ## Correlation Matrix Heat Map Display statistical correlations with diverging color scheme: ```dart theme={null} final correlationData = [ {'var1': 'Revenue', 'var2': 'Marketing', 'correlation': 0.85}, {'var1': 'Revenue', 'var2': 'Sales Team', 'correlation': 0.92}, {'var1': 'Revenue', 'var2': 'Customer Sat', 'correlation': 0.74}, {'var1': 'Marketing', 'var2': 'Revenue', 'correlation': 0.85}, {'var1': 'Marketing', 'var2': 'Sales Team', 'correlation': 0.68}, {'var1': 'Marketing', 'var2': 'Customer Sat', 'correlation': 0.45}, {'var1': 'Sales Team', 'var2': 'Revenue', 'correlation': 0.92}, {'var1': 'Sales Team', 'var2': 'Marketing', 'correlation': 0.68}, {'var1': 'Sales Team', 'var2': 'Customer Sat', 'correlation': 0.58}, {'var1': 'Customer Sat', 'var2': 'Revenue', 'correlation': 0.74}, {'var1': 'Customer Sat', 'var2': 'Marketing', 'correlation': 0.45}, {'var1': 'Customer Sat', 'var2': 'Sales Team', 'correlation': 0.58}, ]; CristalyseChart() .data(correlationData) .mappingHeatMap(x: 'var1', y: 'var2', value: 'correlation') .geomHeatMap( cellSpacing: 2.0, cellBorderRadius: BorderRadius.circular(3), showValues: true, minValue: -1.0, maxValue: 1.0, // Diverging gradient: negative (blue) → neutral (white) → positive (red) colorGradient: [ Colors.blue.shade700, Colors.white, Colors.red.shade700, ], interpolateColors: true, valueFormatter: (value) => value.toStringAsFixed(2), valueTextStyle: TextStyle(fontSize: 11), cellAspectRatio: 1.0, // Square cells ) .scaleXOrdinal() .scaleYOrdinal() .theme(ChartTheme.defaultTheme()) .build() ``` ## Styling Options ### Color Gradients Define custom color schemes for different data types: ```dart theme={null} // Single color gradient (low to high) CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap( colorGradient: [Colors.white, Colors.deepPurple], interpolateColors: true, ) .build() // Multi-step gradient CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap( colorGradient: [ Colors.blue.shade900, Colors.blue.shade300, Colors.white, Colors.orange.shade300, Colors.red.shade900, ], interpolateColors: true, ) .build() // Diverging gradient CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap( colorGradient: [Colors.blue, Colors.white, Colors.red], interpolateColors: true, ) .build() ``` ### Cell Styling Customize cell appearance and borders: ```dart theme={null} CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap( cellSpacing: 3.0, cellBorderRadius: BorderRadius.circular(8), cellAspectRatio: 1.5, // width:height ratio showValues: true, ) .build() ``` ### Value Text Styling Control value text appearance: ```dart theme={null} CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap( showValues: true, valueTextStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, ), ) .build() ``` ## Data Handling ### Missing Values Handle null or missing data points: ```dart theme={null} CristalyseChart() .data(dataWithNulls) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap( nullValueColor: Colors.grey.shade200, ) .build() ``` ### Value Ranges Set explicit minimum and maximum values: ```dart theme={null} CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap( minValue: 0, maxValue: 100, // values outside range are clamped colorGradient: [Colors.white, Colors.red], interpolateColors: true, ) .build() ``` ### Value Formatting Customize how values are displayed: ```dart theme={null} import 'package:intl/intl.dart'; // Currency formatting CristalyseChart() .data(salesData) .mappingHeatMap(x: 'month', y: 'region', value: 'revenue') .geomHeatMap( showValues: true, valueFormatter: (value) => NumberFormat.simpleCurrency().format(value), ) .build() // Percentage formatting CristalyseChart() .data(percentageData) .mappingHeatMap(x: 'category', y: 'segment', value: 'percentage') .geomHeatMap( showValues: true, valueFormatter: (value) => '${(value * 100).toStringAsFixed(1)}%', ) .build() // Custom formatting CristalyseChart() .data(temperatureData) .mappingHeatMap(x: 'hour', y: 'day', value: 'temperature') .geomHeatMap( showValues: true, valueFormatter: (value) => '${value.toStringAsFixed(1)}°C', ) .build() ``` ## Interactive Features ### Hover Effects Add rich tooltips on cell hover: ```dart theme={null} CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap() .interaction( tooltip: TooltipConfig( builder: (point) { return Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black.withOpacity(0.8), borderRadius: BorderRadius.circular(6), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${point.getDisplayValue('x')} - ${point.getDisplayValue('y')}', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), Text( 'Value: ${point.getDisplayValue('value')}', style: TextStyle(color: Colors.white), ), ], ), ); }, ), ) .build() ``` ### Click Handlers React to cell selection: ```dart theme={null} CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap() .interaction( click: ClickConfig( onTap: (point) { print('Clicked cell: ${point.data}'); // Show detailed view showCellDetails(point.data); }, ), ) .build() ``` ## Animation Options ### Fade In Animation Cells appear with smooth fade transition: ```dart theme={null} CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap() .animate( duration: Duration(milliseconds: 1000), curve: Curves.easeInOut, ) .build() ``` ### Staggered Animation Each cell animates with a slight delay: ```dart theme={null} CristalyseChart() .data(data) .mappingHeatMap(x: 'x', y: 'y', value: 'value') .geomHeatMap() .animate( duration: Duration(milliseconds: 1500), curve: Curves.elasticOut, stagger: Duration(milliseconds: 50), ) .build() ``` ## Best Practices ### When to Use Heat Maps **Good for:** * 2D categorical data visualization * Correlation matrices * Time-based patterns (hour vs day) * Geographic data on grids * Performance monitoring dashboards **Avoid for:** * Continuous spatial data * Data with more than 20x20 cells * Precise value comparison * Single-dimension data ### Design Tips * Use intuitive color schemes (cool to warm for intensity) * Ensure sufficient color contrast for accessibility * Limit grid size to maintain readability * Consider showing values for precise reading * Use diverging colors for data with meaningful zero point ### Performance Considerations * Optimize for datasets with \< 400 cells (20x20) * Use simpler styling for large grids * Consider data aggregation for very large datasets * Test color schemes for colorblind accessibility ## Common Patterns ### Website Analytics Heat Map ```dart theme={null} final analyticsData = [ {'page': 'Home', 'hour': '9 AM', 'visitors': 245}, {'page': 'Home', 'hour': '12 PM', 'visitors': 380}, {'page': 'Home', 'hour': '6 PM', 'visitors': 520}, {'page': 'Products', 'hour': '9 AM', 'visitors': 180}, {'page': 'Products', 'hour': '12 PM', 'visitors': 290}, {'page': 'Products', 'hour': '6 PM', 'visitors': 350}, ]; CristalyseChart() .data(analyticsData) .mappingHeatMap(x: 'hour', y: 'page', value: 'visitors') .geomHeatMap( cellSpacing: 2.0, showValues: true, colorGradient: [Colors.blue.shade100, Colors.blue.shade800], interpolateColors: true, ) .build() ``` ### Financial Risk Matrix ```dart theme={null} final riskData = [ {'probability': 'Low', 'impact': 'Low', 'risk': 1}, {'probability': 'Low', 'impact': 'Medium', 'risk': 2}, {'probability': 'Low', 'impact': 'High', 'risk': 3}, {'probability': 'Medium', 'impact': 'Low', 'risk': 2}, {'probability': 'Medium', 'impact': 'Medium', 'risk': 4}, {'probability': 'Medium', 'impact': 'High', 'risk': 6}, {'probability': 'High', 'impact': 'Low', 'risk': 3}, {'probability': 'High', 'impact': 'Medium', 'risk': 6}, {'probability': 'High', 'impact': 'High', 'risk': 9}, ]; CristalyseChart() .data(riskData) .mappingHeatMap(x: 'probability', y: 'impact', value: 'risk') .geomHeatMap( cellSpacing: 2.0, cellAspectRatio: 1.25, showValues: true, colorGradient: [Colors.green, Colors.yellow, Colors.red], interpolateColors: true, ) .build() ``` ## Next Steps Explore relationships between continuous variables Add advanced interactive features to charts Customize colors and styling themes Save heat maps as images or data files # Line Charts Source: https://docs.cristalyse.com/charts/line-charts Time series and trend analysis with multi-series support See line charts in action with interactive examples ## Overview Line charts are essential for displaying trends and patterns in data over a period. Cristalyse provides an elegant API for crafting intuitive line charts.
Progressive Line Chart
Progressive line drawing with customizable themes and multi-series support
## Basic Line Chart Display a simple line chart to show trends: ```dart theme={null} data = [ {'month': 'Jan', 'value': 30}, {'month': 'Feb', 'value': 45}, {'month': 'Mar', 'value': 60}, ]; CristalyseChart() .data(data) .mapping(x: 'month', y: 'value') .geomLine(strokeWidth: 2.0, color: Colors.blue) .scaleXOrdinal() .scaleYContinuous() .build(); ``` ## Multi-Series Line Chart Compare series by plotting multiple lines: ```dart theme={null} data = [ {'month': 'Jan', 'platform': 'iOS', 'users': 1200}, {'month': 'Jan', 'platform': 'Android', 'users': 800}, {'month': 'Feb', 'platform': 'iOS', 'users': 1350}, {'month': 'Feb', 'platform': 'Android', 'users': 950}, ]; CristalyseChart() .data(data) .mapping(x: 'month', y: 'users', color: 'platform') .geomLine(strokeWidth: 3.0) .geomPoint(size: 4.0) .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .build(); ``` ## Progressive Drawing Animate lines progressively from start to end: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'value') .geomLine(strokeWidth: 2.0) .animate(duration: Duration(milliseconds: 1500)) .build(); ``` ## Styling Lines ### Dashed and Dotted Lines Change line styles for variety and emphasis: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'value') .geomLine( strokeWidth: 3.0, style: LineStyle.dashed, ) .build(); ``` ### Highlighting Points Enhance visual storytelling by emphasizing points: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'value') .geomLine(strokeWidth: 2.0) .geomPoint(size: 6.0, color: Colors.red) .build(); ``` ## Custom Category Colors Assign specific colors to line series for brand consistency or semantic meaning: ```dart theme={null} // Product launch performance tracking final productColors = { 'Premium Edition': const Color(0xFFAB47BC), // Purple - luxury 'Standard Edition': const Color(0xFF42A5F5), // Blue - reliable 'Lite Edition': const Color(0xFF66BB6A), // Green - accessible 'Beta Version': const Color(0xFFFF7043), // Orange - experimental }; CristalyseChart() .data(launchMetrics) .mapping(x: 'week', y: 'downloads', color: 'product') .geomLine(strokeWidth: 2.5) .geomPoint(size: 4.0) .customPalette(categoryColors: productColors) .build(); ``` ### Performance Status Lines Use semantic colors for status or performance indicators: ```dart theme={null} // Network latency by data center final dcColors = { 'US-East': const Color(0xFF1E88E5), // Blue - primary 'US-West': const Color(0xFF43A047), // Green - secondary 'EU-Central': const Color(0xFFE53935), // Red - international 'Asia-Pacific': const Color(0xFFFF9800), // Orange - distant }; CristalyseChart() .data(latencyData) .mapping(x: 'timestamp', y: 'latency_ms', color: 'datacenter') .geomLine(strokeWidth: 2.0, alpha: 0.8) .customPalette(categoryColors: dcColors) .build(); ``` ## Interactive Features ### Tooltips Provide context with informative tooltips: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'value') .geomLine() .interaction( tooltip: TooltipConfig( builder: (point) { return Text( 'Value: ${point.getDisplayValue('value')}', style: TextStyle(color: Colors.white), ); }, ), ) .build(); ``` ## Performance Optimizations Cristalyse is designed to handle large datasets efficiently. For optimal performance: * Keep stroke width moderate * Use lightweight themes * Optimize data points and mapping categories Ready to explore more chart types? [Back to Chart Gallery](charts/area-charts) # Pie Charts Source: https://docs.cristalyse.com/charts/pie-charts Visualize proportions and percentages with animated pie and donut charts See pie charts in action with interactive examples ## Overview Pie charts are perfect for displaying proportional data and percentages. Cristalyse supports both traditional pie charts and donut charts with smooth animations, customizable styling, and interactive features.
Pie and donut charts with slice-by-slice progressive animation
## Basic Pie Chart Create a simple pie chart to show proportions: ```dart theme={null} final marketData = [ {'company': 'Apple', 'marketShare': 28.5}, {'company': 'Samsung', 'marketShare': 22.1}, {'company': 'Google', 'marketShare': 15.8}, {'company': 'Xiaomi', 'marketShare': 12.3}, {'company': 'Others', 'marketShare': 21.3}, ]; CristalyseChart() .data(marketData) .mapping( pieValue: 'marketShare', pieCategory: 'company', ) .geomPie( outerRadius: 150, showLabels: true, showPercentages: true, ) .theme(ChartTheme.defaultTheme()) .build() ``` ## Donut Chart Create a donut chart with a hollow center: ```dart theme={null} final salesData = [ {'region': 'North America', 'sales': 450000}, {'region': 'Europe', 'sales': 320000}, {'region': 'Asia Pacific', 'sales': 280000}, {'region': 'Latin America', 'sales': 150000}, {'region': 'Middle East', 'sales': 100000}, ]; CristalyseChart() .data(salesData) .mapping( pieValue: 'sales', pieCategory: 'region', ) .geomPie( outerRadius: 180, innerRadius: 80, // Creates donut effect showLabels: true, showPercentages: false, ) .theme(ChartTheme.defaultTheme()) .build() ``` ## Exploded Pie Chart Create separation between slices for emphasis: ```dart theme={null} final budgetData = [ {'category': 'Development', 'amount': 450000}, {'category': 'Marketing', 'amount': 280000}, {'category': 'Operations', 'amount': 320000}, {'category': 'Sales', 'amount': 180000}, ]; CristalyseChart() .data(budgetData) .mapping( pieValue: 'amount', pieCategory: 'category', ) .geomPie( outerRadius: 160, explodeSlices: true, explodeDistance: 15, showLabels: true, showPercentages: true, ) .theme(ChartTheme.defaultTheme()) .build() ``` ## Styling Options ### Custom Colors Override default color mapping: ```dart theme={null} CristalyseChart() .data(data) .mapping( pieValue: 'value', pieCategory: 'category', ) .geomPie( outerRadius: 150, colors: [ Colors.blue, Colors.green, Colors.orange, Colors.red, Colors.purple, ], ) .build() ``` ### Stroke Styling Add borders to pie slices: ```dart theme={null} CristalyseChart() .data(data) .mapping( pieValue: 'value', pieCategory: 'category', ) .geomPie( outerRadius: 150, strokeWidth: 2.0, strokeColor: Colors.white, showLabels: true, ) .build() ``` ### Custom Label Styling Customize label appearance: ```dart theme={null} CristalyseChart() .data(data) .mapping( pieValue: 'value', pieCategory: 'category', ) .geomPie( outerRadius: 150, showLabels: true, showPercentages: true, labelRadius: 180, labelStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, ), ) .build() ``` ## Animation Options ### Progressive Slice Animation Slices animate in sequence: ```dart theme={null} CristalyseChart() .data(data) .mapping( pieValue: 'value', pieCategory: 'category', ) .geomPie( outerRadius: 150, animationDuration: Duration(milliseconds: 1500), ) .animate( duration: Duration(milliseconds: 1500), curve: Curves.easeInOutCubic, ) .build() ``` ### Custom Start Angle Control where the pie starts: ```dart theme={null} CristalyseChart() .data(data) .mapping( pieValue: 'value', pieCategory: 'category', ) .geomPie( outerRadius: 150, startAngle: -pi / 2, // Start at top (12 o'clock) ) .build() ``` ## Label Formatting Transform pie chart labels by passing a callback to the `labels` parameter. ### Simple Custom Labels For basic formatting, you can use a simple callback: ```dart theme={null} CristalyseChart() .data(salesData) .mappingPie(value: 'revenue', category: 'region') .geomPie( outerRadius: 150, showLabels: true, showPercentages: false, labels: (value) => '\$${value}K', // $450K ) .build() ``` ### Using NumberFormat For robust, locale-aware formatting, use NumberFormat: ```dart theme={null} import 'package:intl/intl.dart'; // Currency formatting CristalyseChart() .data(revenueData) .mappingPie(value: 'revenue', category: 'department') .geomPie( outerRadius: 150, showLabels: true, showPercentages: false, labels: NumberFormat.simpleCurrency().format, // $1,234.56 ) .build() // Compact user counts CristalyseChart() .data(userData) .mappingPie(value: 'users', category: 'platform') .geomPie( outerRadius: 150, showLabels: true, showPercentages: false, labels: NumberFormat.compact().format, // 1.2K ) .build() ``` ### Percentage Formatting When `showPercentages: true`, your callback receives ratio values (0.0-1.0): ```dart theme={null} // Default percentage formatting CristalyseChart() .data(surveyData) .mappingPie(value: 'responses', category: 'rating') .geomPie( outerRadius: 150, showLabels: true, showPercentages: true, // Uses NumberFormat.percentPattern() ) .build() // Custom percentage with NumberFormat CristalyseChart() .data(surveyData) .mappingPie(value: 'responses', category: 'rating') .geomPie( outerRadius: 150, showLabels: true, showPercentages: true, labels: NumberFormat.percentPattern().format, // 23% ) .build() ``` ## Real-World Examples ### E-commerce Sales by Category ```dart theme={null} final ecommerceData = [ {'category': 'Electronics', 'sales': 2850000, 'color': Colors.blue}, {'category': 'Clothing', 'sales': 1920000, 'color': Colors.green}, {'category': 'Home & Garden', 'sales': 1450000, 'color': Colors.orange}, {'category': 'Books', 'sales': 890000, 'color': Colors.red}, {'category': 'Sports', 'sales': 720000, 'color': Colors.purple}, ]; CristalyseChart() .data(ecommerceData) .mapping( pieValue: 'sales', pieCategory: 'category', ) .geomPie( outerRadius: 180, innerRadius: 60, showLabels: true, showPercentages: true, labelRadius: 220, strokeWidth: 3, strokeColor: Colors.white, explodeSlices: true, explodeDistance: 8, ) .theme(ChartTheme.defaultTheme()) .build() ``` ### Project Time Allocation ```dart theme={null} final projectData = [ {'phase': 'Planning', 'hours': 120}, {'phase': 'Development', 'hours': 480}, {'phase': 'Testing', 'hours': 160}, {'phase': 'Deployment', 'hours': 80}, {'phase': 'Documentation', 'hours': 60}, ]; CristalyseChart() .data(projectData) .mapping( pieValue: 'hours', pieCategory: 'phase', ) .geomPie( outerRadius: 160, showLabels: true, showPercentages: true, labelStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ), ) .theme(ChartTheme.defaultTheme()) .build() ``` ## Advanced Features ### Interactive Pie Charts Add hover effects and click handling: ```dart theme={null} CristalyseChart() .data(data) .mapping( pieValue: 'value', pieCategory: 'category', ) .geomPie( outerRadius: 150, showLabels: true, onSliceClick: (data, index) { print('Clicked slice: ${data['category']}'); }, ) .interactions( enableHover: true, enableClick: true, ) .build() ``` ### Responsive Sizing Automatically adjust based on container size: ```dart theme={null} LayoutBuilder( builder: (context, constraints) { final radius = math.min(constraints.maxWidth, constraints.maxHeight) / 3; return CristalyseChart() .data(data) .mapping( pieValue: 'value', pieCategory: 'category', ) .geomPie( outerRadius: radius, innerRadius: radius * 0.4, showLabels: constraints.maxWidth > 300, ) .build(); }, ) ``` ## Best Practices ### When to Use Pie Charts **Good for:** * Showing parts of a whole * Displaying percentages * Comparing proportions (5-7 categories max) * Simple data visualization **Avoid for:** * Too many categories (>7) * Comparing exact values * Time series data * Negative values ### Design Tips * Limit to 5-7 slices for readability * Use contrasting colors * Order slices by size (largest first) * Consider donut charts for modern look * Use exploded slices sparingly for emphasis ### Performance Considerations * Optimize for large datasets by grouping small values * Use appropriate animation durations * Consider static rendering for print/export * Test on different screen sizes ## Common Patterns ### Market Share Analysis ```dart theme={null} final marketShare = [ {'company': 'Leader', 'share': 35.2}, {'company': 'Challenger', 'share': 24.8}, {'company': 'Follower', 'share': 18.5}, {'company': 'Niche', 'share': 12.1}, {'company': 'Others', 'share': 9.4}, ]; CristalyseChart() .data(marketShare) .mapping(pieValue: 'share', pieCategory: 'company') .geomPie( outerRadius: 160, showLabels: true, showPercentages: true, explodeSlices: true, explodeDistance: 10, ) .build() ``` ### Survey Results ```dart theme={null} final surveyResults = [ {'response': 'Very Satisfied', 'count': 45}, {'response': 'Satisfied', 'count': 38}, {'response': 'Neutral', 'count': 12}, {'response': 'Dissatisfied', 'count': 3}, {'response': 'Very Dissatisfied', 'count': 2}, ]; CristalyseChart() .data(surveyResults) .mapping(pieValue: 'count', pieCategory: 'response') .geomPie( outerRadius: 150, innerRadius: 50, showLabels: true, showPercentages: true, ) .build() ``` # Progress Bars Source: https://docs.cristalyse.com/charts/progress-bars Professional progress visualization with multiple orientations and advanced styles See progress bars in action with interactive examples ## Overview Progress bars are essential for visualizing completion status, goal achievement, and performance metrics. Cristalyse provides comprehensive progress bar support with multiple orientations and advanced styling options perfect for dashboards, project management tools, and analytics applications. ## Basic Progress Bars ### Horizontal Progress Bar The classic left-to-right progress visualization: ```dart theme={null} final taskData = [ {'task': 'Design', 'completion': 75.0}, {'task': 'Development', 'completion': 60.0}, {'task': 'Testing', 'completion': 30.0}, ]; CristalyseChart() .data(taskData) .mappingProgress(category: 'task', value: 'completion') .geomProgress( orientation: ProgressOrientation.horizontal, barThickness: 20.0, cornerRadius: 10.0, showLabels: true, ) .scaleYContinuous(min: 0, max: 100) .build() ``` ### Vertical Progress Bar Bottom-to-top progress display: ```dart theme={null} CristalyseChart() .data(taskData) .mappingProgress(category: 'task', value: 'completion') .geomProgress( orientation: ProgressOrientation.vertical, barThickness: 30.0, cornerRadius: 15.0, showLabels: true, ) .scaleXContinuous(min: 0, max: 100) .build() ``` ### Circular Progress Bar Ring-style progress indicators: ```dart theme={null} CristalyseChart() .data([{'metric': 'CPU Usage', 'value': 68.0}]) .mappingProgress(category: 'metric', value: 'value') .geomProgress( orientation: ProgressOrientation.circular, barThickness: 15.0, startAngle: -90.0, // Start at top endAngle: 270.0, // Full circle ) .build() ``` ## Advanced Styles ### Stacked Progress Bars Multi-segment progress showing completion by category: ```dart theme={null} final projectData = [ {'project': 'Mobile App', 'stage': 'Design', 'progress': 30.0}, {'project': 'Mobile App', 'stage': 'Development', 'progress': 50.0}, {'project': 'Mobile App', 'stage': 'Testing', 'progress': 20.0}, ]; CristalyseChart() .data(projectData) .mappingProgress( category: 'project', value: 'progress', group: 'stage', // Groups create segments ) .geomProgress( style: ProgressStyle.stacked, orientation: ProgressOrientation.horizontal, barThickness: 25.0, showLabels: true, ) .build() ``` **Perfect for:** * Project phase tracking * Budget allocation visualization * Time spent analysis * Multi-category completion ### Grouped Progress Bars Side-by-side progress bars for category comparison: ```dart theme={null} final teamData = [ {'team': 'Engineering', 'metric': 'Sprint Goals', 'achievement': 85.0}, {'team': 'Engineering', 'metric': 'Code Quality', 'achievement': 92.0}, {'team': 'Design', 'metric': 'Sprint Goals', 'achievement': 78.0}, {'team': 'Design', 'metric': 'Code Quality', 'achievement': 88.0}, ]; CristalyseChart() .data(teamData) .mappingProgress( category: 'team', value: 'achievement', group: 'metric', ) .geomProgress( style: ProgressStyle.grouped, orientation: ProgressOrientation.vertical, barThickness: 20.0, ) .legend() .build() ``` **Perfect for:** * Team performance comparison * KPI tracking across departments * A/B test results * Multi-metric dashboards ### Gauge Style Semi-circular gauge-style indicators: ```dart theme={null} CristalyseChart() .data([ {'metric': 'CPU', 'usage': 68.0}, {'metric': 'Memory', 'usage': 82.0}, {'metric': 'Disk', 'usage': 45.0}, ]) .mappingProgress(category: 'metric', value: 'usage') .geomProgress( style: ProgressStyle.gauge, orientation: ProgressOrientation.circular, gaugeRadius: 120.0, // Required for gauge style barThickness: 18.0, startAngle: -135.0, // Bottom-left endAngle: 135.0, // Bottom-right (270° arc) showLabels: true, ) .build() ``` **Required Parameter:** Gauge style progress bars require the `gaugeRadius` parameter to be specified. **Perfect for:** * System resource monitoring * Performance dashboards * Speed/capacity indicators * Real-time metrics ### Concentric Circles Nested circular progress rings for multi-metric displays: ```dart theme={null} final metricsData = [ {'metric': 'Sales Target', 'achievement': 85.0}, {'metric': 'Quality Score', 'achievement': 92.0}, {'metric': 'Customer Satisfaction', 'achievement': 78.0}, ]; CristalyseChart() .data(metricsData) .mappingProgress(category: 'metric', value: 'achievement') .geomProgress( style: ProgressStyle.concentric, orientation: ProgressOrientation.circular, barThickness: 12.0, startAngle: -90.0, endAngle: 270.0, ) .legend(position: LegendPosition.bottom) .build() ``` **Perfect for:** * Multi-KPI dashboards * Goal achievement tracking * Health/fitness metrics * Balanced scorecard visualizations ## Styling & Customization ### Colors & Themes Progress bars automatically adapt to your theme: ```dart theme={null} CristalyseChart() .data(taskData) .mappingProgress(category: 'task', value: 'completion', group: 'priority') .geomProgress( style: ProgressStyle.stacked, orientation: ProgressOrientation.horizontal, ) .theme(ChartTheme.darkTheme()) // Dark mode support .legend() .build() ``` ### Custom Colors Use custom color palettes for semantic meaning: ```dart theme={null} final statusColors = { 'Critical': Colors.red, 'Warning': Colors.orange, 'Normal': Colors.green, }; CristalyseChart() .data(systemData) .mappingProgress(category: 'system', value: 'health', group: 'status') .geomProgress(style: ProgressStyle.grouped) .customPalette(categoryColors: statusColors) .build() ``` ### Track Colors Customize the background track color: ```dart theme={null} CristalyseChart() .data(taskData) .mappingProgress(category: 'task', value: 'completion') .geomProgress( orientation: ProgressOrientation.horizontal, trackColor: Colors.grey.shade200, // Custom track barThickness: 20.0, cornerRadius: 10.0, ) .build() ``` ## Labels & Formatting ### Show Percentage Labels Display completion percentages: ```dart theme={null} CristalyseChart() .data(taskData) .mappingProgress(category: 'task', value: 'completion') .geomProgress( orientation: ProgressOrientation.horizontal, showLabels: true, labelFormatter: (value) => '${value.toStringAsFixed(1)}%', labelStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ) .build() ``` ### Custom Label Formatting ```dart theme={null} import 'package:intl/intl.dart'; // Currency formatting for budget progress CristalyseChart() .data(budgetData) .mappingProgress(category: 'department', value: 'spent') .geomProgress( showLabels: true, labelFormatter: (value) => NumberFormat.simpleCurrency().format(value), ) .build() ``` ## Interactive Features ### Tooltips Add hover information: ```dart theme={null} CristalyseChart() .data(taskData) .mappingProgress(category: 'task', value: 'completion') .geomProgress(orientation: ProgressOrientation.horizontal) .interaction( tooltip: TooltipConfig( builder: (point) { final task = point.getDisplayValue('task'); final completion = point.getDisplayValue('completion'); return Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(8), ), child: Text( '$task: $completion%', style: TextStyle(color: Colors.white), ), ); }, ), ) .build() ``` ### Click Handlers Respond to user interactions: ```dart theme={null} CristalyseChart() .data(taskData) .mappingProgress(category: 'task', value: 'completion') .geomProgress(orientation: ProgressOrientation.horizontal) .interaction( click: ClickConfig( onTap: (point) { final task = point.getDisplayValue('task'); showTaskDetails(context, task); }, ), ) .build() ``` ## Animations ### Progressive Filling Animate progress filling: ```dart theme={null} CristalyseChart() .data(taskData) .mappingProgress(category: 'task', value: 'completion') .geomProgress( orientation: ProgressOrientation.horizontal, barThickness: 20.0, ) .animate( duration: Duration(milliseconds: 1200), curve: Curves.easeOutCubic, ) .build() ``` ### Circular Animations Smooth circular progress animations: ```dart theme={null} CristalyseChart() .data([{'metric': 'Loading', 'progress': 75.0}]) .mappingProgress(category: 'metric', value: 'progress') .geomProgress( orientation: ProgressOrientation.circular, barThickness: 15.0, ) .animate( duration: Duration(milliseconds: 1500), curve: Curves.elasticOut, ) .build() ``` ## Real-World Examples ### Project Management Dashboard ```dart theme={null} final projectData = [ {'project': 'Website Redesign', 'phase': 'Planning', 'progress': 100.0}, {'project': 'Website Redesign', 'phase': 'Design', 'progress': 80.0}, {'project': 'Website Redesign', 'phase': 'Development', 'progress': 45.0}, {'project': 'Website Redesign', 'phase': 'Testing', 'progress': 0.0}, ]; CristalyseChart() .data(projectData) .mappingProgress( category: 'project', value: 'progress', group: 'phase', ) .geomProgress( style: ProgressStyle.stacked, orientation: ProgressOrientation.horizontal, barThickness: 30.0, cornerRadius: 15.0, showLabels: true, ) .theme(ChartTheme.defaultTheme()) .legend(position: LegendPosition.bottom) .build() ``` ### System Monitoring Dashboard ```dart theme={null} final systemMetrics = [ {'resource': 'CPU', 'usage': 68.0}, {'resource': 'Memory', 'usage': 82.0}, {'resource': 'Disk', 'usage': 45.0}, {'resource': 'Network', 'usage': 35.0}, ]; final statusColors = { 'CPU': Colors.blue, 'Memory': Colors.orange, 'Disk': Colors.green, 'Network': Colors.purple, }; CristalyseChart() .data(systemMetrics) .mappingProgress(category: 'resource', value: 'usage') .geomProgress( style: ProgressStyle.gauge, orientation: ProgressOrientation.circular, gaugeRadius: 100.0, barThickness: 16.0, startAngle: -135.0, endAngle: 135.0, showLabels: true, ) .customPalette(categoryColors: statusColors) .legend(position: LegendPosition.right) .build() ``` ### Fitness Tracker ```dart theme={null} final fitnessGoals = [ {'goal': 'Daily Steps', 'achievement': 85.0}, {'goal': 'Calories Burned', 'achievement': 92.0}, {'goal': 'Active Minutes', 'achievement': 78.0}, {'goal': 'Sleep Hours', 'achievement': 95.0}, ]; CristalyseChart() .data(fitnessGoals) .mappingProgress(category: 'goal', value: 'achievement') .geomProgress( style: ProgressStyle.concentric, orientation: ProgressOrientation.circular, barThickness: 14.0, startAngle: -90.0, endAngle: 270.0, ) .theme(ChartTheme.solarizedLightTheme()) .legend(position: LegendPosition.bottom) .animate(duration: Duration(milliseconds: 1500)) .build() ``` ## Configuration Options ### ProgressOrientation * `horizontal` - Left-to-right progress * `vertical` - Bottom-to-top progress * `circular` - Ring-style circular progress ### ProgressStyle * `standard` - Single progress bar (default) * `stacked` - Multi-segment stacked progress * `grouped` - Side-by-side grouped progress * `gauge` - Semi-circular gauge (requires `gaugeRadius`) * `concentric` - Nested circular rings ### Common Parameters | Parameter | Type | Description | | ---------------- | ----------- | ------------------------------------------------------- | | `barThickness` | `double` | Height (horizontal) or width (vertical) of progress bar | | `cornerRadius` | `double` | Border radius for rounded corners | | `trackColor` | `Color` | Background track color | | `showLabels` | `bool` | Display value labels | | `labelFormatter` | `Function` | Custom label formatting function | | `labelStyle` | `TextStyle` | Text style for labels | | `startAngle` | `double` | Start angle for circular progress (degrees) | | `endAngle` | `double` | End angle for circular progress (degrees) | | `gaugeRadius` | `double` | Radius for gauge-style progress (required for gauge) | ## Best Practices ### Data Structure Ensure your data has the required fields: ```dart theme={null} // Single progress bar final data = [ {'category': 'Task Name', 'value': 75.0}, ]; // Grouped/Stacked progress final data = [ {'category': 'Project A', 'value': 30.0, 'group': 'Phase 1'}, {'category': 'Project A', 'value': 50.0, 'group': 'Phase 2'}, ]; ``` ### Value Ranges Keep values within meaningful ranges: ```dart theme={null} // Percentages: 0-100 .scaleYContinuous(min: 0, max: 100) // Absolute values .scaleYContinuous(min: 0, max: 1000) ``` ### Performance For real-time updates: ```dart theme={null} // Use StatefulWidget for live progress class LiveProgress extends StatefulWidget { @override _LiveProgressState createState() => _LiveProgressState(); } class _LiveProgressState extends State { double _progress = 0.0; @override void initState() { super.initState(); // Update progress periodically Timer.periodic(Duration(seconds: 1), (timer) { setState(() => _progress = (_progress + 5) % 100); }); } @override Widget build(BuildContext context) { return CristalyseChart() .data([{'task': 'Processing', 'value': _progress}]) .mappingProgress(category: 'task', value: 'value') .geomProgress(orientation: ProgressOrientation.horizontal) .build(); } } ``` ## Troubleshooting Make sure you've specified the required `gaugeRadius` parameter: ```dart theme={null} .geomProgress( style: ProgressStyle.gauge, gaugeRadius: 120.0, // Required! ) ``` Adjust the `barThickness` parameter: ```dart theme={null} .geomProgress( barThickness: 25.0, // Increase for thicker bars ) ``` Ensure your data includes the `group` parameter in mapping: ```dart theme={null} .mappingProgress( category: 'project', value: 'progress', group: 'stage', // Required for stacked/grouped ) ``` Adjust the `startAngle` parameter: ```dart theme={null} .geomProgress( startAngle: -90.0, // Start at top (12 o'clock) endAngle: 270.0, // Full circle ) ``` ## Related Documentation * [Animations](/features/animations) - Add smooth progress animations * [Theming](/features/theming) - Customize progress bar appearance * [Interactions](/features/interactions) - Add tooltips and click handlers * [Legends](/features/legends) - Display category legends Need help? Check our [GitHub Discussions](https://github.com/rudi-q/cristalyse/discussions) or [report an issue](https://github.com/rudi-q/cristalyse/issues). # Scatter Plots Source: https://docs.cristalyse.com/charts/scatter-plots Point-based visualizations with size and color mapping See scatter plots in action with interactive examples ## Overview Scatter plots are perfect for exploring relationships between two continuous variables. Cristalyse's scatter plots support multi-dimensional encoding through color, size, and shape mappings.
Interactive Scatter Plot Animation
Interactive scatter plots with smooth animations and multi-dimensional data mapping
## Basic Scatter Plot The simplest scatter plot maps X and Y coordinates to data: ```dart theme={null} final data = [ {'x': 1, 'y': 2}, {'x': 2, 'y': 3}, {'x': 3, 'y': 1}, {'x': 4, 'y': 4}, ]; CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .scaleXContinuous() .scaleYContinuous() .build() ``` ## Color Mapping Add categorical grouping with color: ```dart theme={null} final data = [ {'x': 1, 'y': 2, 'category': 'A'}, {'x': 2, 'y': 3, 'category': 'B'}, {'x': 3, 'y': 1, 'category': 'A'}, {'x': 4, 'y': 4, 'category': 'C'}, ]; CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint(size: 8.0, alpha: 0.8) .scaleXContinuous() .scaleYContinuous() .theme(ChartTheme.defaultTheme()) .build() ``` ## Size Mapping Encode a third dimension with point size: ```dart theme={null} final salesData = [ {'revenue': 100, 'deals': 25, 'region': 'North', 'team_size': 5}, {'revenue': 150, 'deals': 30, 'region': 'South', 'team_size': 8}, {'revenue': 80, 'deals': 20, 'region': 'East', 'team_size': 3}, {'revenue': 200, 'deals': 35, 'region': 'West', 'team_size': 12}, ]; CristalyseChart() .data(salesData) .mapping( x: 'revenue', y: 'deals', color: 'region', size: 'team_size' ) .geomPoint(alpha: 0.7) .scaleXContinuous(min: 0) .scaleYContinuous(min: 0) .build() ``` ## Point Styling ### Shape Options Customize point shapes for different data categories: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint( size: 10.0, shape: PointShape.triangle, // circle, square, triangle, diamond borderWidth: 2.0, alpha: 0.8, ) .build() ``` Available shapes: * `PointShape.circle` (default) * `PointShape.square` * `PointShape.triangle` * `PointShape.diamond` ### Advanced Styling ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint( size: 12.0, alpha: 0.8, shape: PointShape.circle, borderWidth: 1.5, // Border thickness color: Colors.blue, // Override color mapping ) .build() ``` ## Multi-Dimensional Analysis ### Business Intelligence Example Analyze sales performance across multiple dimensions: ```dart theme={null} final performanceData = [ { 'revenue': 150000, 'customer_satisfaction': 4.2, 'region': 'North America', 'deal_count': 45, 'rep_experience': 3.5, }, { 'revenue': 120000, 'customer_satisfaction': 3.8, 'region': 'Europe', 'deal_count': 38, 'rep_experience': 2.1, }, { 'revenue': 180000, 'customer_satisfaction': 4.6, 'region': 'Asia Pacific', 'deal_count': 52, 'rep_experience': 5.2, }, ]; CristalyseChart() .data(performanceData) .mapping( x: 'revenue', y: 'customer_satisfaction', color: 'region', size: 'deal_count', ) .geomPoint(alpha: 0.7, borderWidth: 1.0) .scaleXContinuous(min: 0) .scaleYContinuous(min: 1, max: 5) .theme(ChartTheme.defaultTheme()) .build() ``` ## Interactive Scatter Plots ### Tooltips Add rich hover information: ```dart theme={null} CristalyseChart() .data(performanceData) .mapping(x: 'revenue', y: 'customer_satisfaction', color: 'region') .geomPoint(size: 8.0) .interaction( tooltip: TooltipConfig( builder: (point) { final region = point.getDisplayValue('region'); final revenue = point.getDisplayValue('revenue'); final satisfaction = point.getDisplayValue('customer_satisfaction'); return Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(8), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( region, style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), SizedBox(height: 4), Text( 'Revenue: \$${revenue}', style: TextStyle(color: Colors.white), ), Text( 'Satisfaction: ${satisfaction}/5', style: TextStyle(color: Colors.white), ), ], ), ); }, ), ) .build() ``` ### Click Handlers React to point selection: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint(size: 8.0) .interaction( click: ClickConfig( onTap: (point) { print('Selected point: ${point.data}'); // Navigate to detail view // Show dialog // Update other charts }, ), ) .build() ``` ## Animation ### Entrance Animation Animate points appearing: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint(size: 8.0, alpha: 0.8) .animate( duration: Duration(milliseconds: 800), curve: Curves.elasticOut, ) .build() ``` ### Staggered Animation Points appear in sequence: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint(size: 10.0) .animate( duration: Duration(milliseconds: 1200), curve: Curves.easeInOutCubic, ) .build() ``` ## Dual Y-Axis Support Use scatter plots on secondary Y-axis: ```dart theme={null} final mixedData = [ {'quarter': 'Q1', 'revenue': 120, 'efficiency': 85, 'satisfaction': 4.2}, {'quarter': 'Q2', 'revenue': 150, 'efficiency': 92, 'satisfaction': 4.5}, {'quarter': 'Q3', 'revenue': 110, 'efficiency': 78, 'satisfaction': 3.9}, ]; CristalyseChart() .data(mixedData) .mapping(x: 'quarter', y: 'revenue') .mappingY2('satisfaction') .geomBar(yAxis: YAxis.primary) // Revenue bars .geomPoint( // Satisfaction points yAxis: YAxis.secondary, size: 10.0, color: Colors.orange, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .scaleY2Continuous(min: 1, max: 5) .build() ``` ## Best Practices * Use alpha (transparency) for overlapping points * Consider point size relative to data density * For 1000+ points, reduce size and increase alpha * Limit color categories to 8-10 for readability * Use consistent color palettes across charts * Consider colorblind-friendly palettes * Use size for quantitative variables only * Ensure size differences are perceptually meaningful * Avoid extreme size ratios (keep within 2:1 to 5:1) * For large datasets (1000+ points), disable borders * Use lower alpha values for dense datasets * Consider data sampling for very large datasets ## Common Use Cases Explore relationships between variables Identify unusual data points visually Discover natural groupings in data Analyze 3-4 dimensions simultaneously ## Next Steps Connect data points with lines Add tooltips and click handlers Smooth entrance and transition effects Customize colors and visual styles # Cristalyse MCP Server Source: https://docs.cristalyse.com/cristalyse-mcp-server Connect Cristalyse documentation to your AI coding assistant using the Model Context Protocol (MCP) The Cristalyse MCP Server provides AI coding assistants with direct access to Cristalyse documentation, examples, and best practices through the Model Context Protocol (MCP). This enables your AI assistant to provide accurate, up-to-date information about Cristalyse directly in your coding environment. ## What is MCP? The Model Context Protocol (MCP) is a standardized way for AI applications to securely connect to external data sources and tools. By adding the Cristalyse MCP server, your AI assistant gains access to: * Complete Cristalyse documentation * Code examples and snippets * Best practices and patterns * API reference materials * Troubleshooting guides ## Setup Instructions Choose your AI coding assistant below for specific setup instructions: Add the following configuration to your MCP settings file: ```json theme={null} { "mcpServers": { "cristalyse_docs": { "command": "npx", "args": [ "mcp-remote", "https://docs.cristalyse.com/mcp" ] } } } ``` Add the following configuration to your MCP settings file: ```json theme={null} { "mcpServers": { "cristalyse_docs": { "url": "https://docs.cristalyse.com/mcp" } } } ``` Add the following configuration to your MCP settings file: ```json theme={null} { "mcpServers": { "cristalyse_docs": { "url": "https://docs.cristalyse.com/mcp" } } } ``` Add the following configuration to your MCP settings file: ```json theme={null} { "mcpServers": { "cristalyse_docs": { "url": "https://docs.cristalyse.com/mcp" } } } ``` Run the following command in your terminal: ```bash theme={null} claude mcp add --transport http cristalyse_docs https://docs.cristalyse.com/mcp ``` Follow these steps to add the Cristalyse MCP server to Claude: ### Step 1: Add the MCP Server 1. Navigate to the **Connectors** page in the Claude settings 2. Select **Add custom connector** 3. Add the Cristalyse MCP server with these details: * **Name**: `CristalyseDocs` * **URL**: `https://docs.cristalyse.com/mcp` 4. Select **Add** ### Step 2: Use the MCP Server in Your Chat 1. When using Claude, select the **attachments button** (the plus icon) 2. Select the **CristalyseDocs** MCP server 3. Ask Claude any question about Cristalyse documentation ## Example Usage Once connected, you can ask your AI assistant questions like: * "How do I create a scatter plot with Cristalyse?" * "What are the available chart types in Cristalyse?" * "Show me an example of custom theming" * "How do I add animations to my charts?" * "What's the best way to handle large datasets?" The AI assistant will have access to the complete Cristalyse documentation and can provide accurate, contextual responses with code examples. ## Benefits **Always Up-to-Date**: Access the latest documentation directly from the source\ **Context-Aware**: Get relevant examples based on your current code\ **Faster Development**: No need to switch between your IDE and documentation\ **Better Code Quality**: Follow best practices and patterns recommended in the docs\ **Instant Help**: Get immediate answers to Cristalyse-related questions ## Troubleshooting If you're having issues connecting to the MCP server: 1. **Check your internet connection** - The MCP server requires internet access 2. **Verify the URL** - Make sure you're using `https://docs.cristalyse.com/mcp` 3. **Restart your AI assistant** - Sometimes a restart is needed after adding new MCP servers 4. **Check your MCP settings** - Ensure the JSON configuration is valid Need help? Visit our [Questions section](https://github.com/rudi-q/cristalyse/discussions/categories/q-a) for community support. # Examples Source: https://docs.cristalyse.com/examples Real-world chart examples and use cases Want to see Cristalyse in action? [Try our interactive demo →](https://example.cristalyse.com) ## Chart Gallery Explore different chart types and their use cases with complete code examples. Point-based visualizations with size and color mapping Time series and trend analysis Categorical data with multiple variations Volume and cumulative data visualization Business dashboards with multiple metrics Tooltips, pan/zoom, and click handlers ## Scatter Plots ### Basic Scatter Plot Perfect for exploring relationships between two variables: ```dart theme={null} final data = [ {'x': 1, 'y': 2.3, 'category': 'A'}, {'x': 2, 'y': 3.1, 'category': 'B'}, {'x': 3, 'y': 1.8, 'category': 'A'}, {'x': 4, 'y': 4.2, 'category': 'C'}, {'x': 5, 'y': 2.9, 'category': 'B'}, ]; CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint( size: 8.0, alpha: 0.8, shape: PointShape.circle, borderWidth: 1.5, ) .scaleXContinuous() .scaleYContinuous() .theme(ChartTheme.defaultTheme()) .animate(duration: Duration(milliseconds: 800)) .build() ``` ### Multi-Dimensional Scatter Plot Using size to encode a third dimension: ```dart theme={null} final salesData = [ {'revenue': 100, 'deals': 25, 'region': 'North', 'rep_count': 5}, {'revenue': 150, 'deals': 30, 'region': 'South', 'rep_count': 8}, {'revenue': 80, 'deals': 20, 'region': 'East', 'rep_count': 3}, {'revenue': 200, 'deals': 35, 'region': 'West', 'rep_count': 12}, ]; CristalyseChart() .data(salesData) .mapping(x: 'revenue', y: 'deals', color: 'region', size: 'rep_count') .geomPoint(alpha: 0.7) .scaleXContinuous(min: 0) .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .build() ``` ## Line Charts ### Time Series Classic time series visualization: ```dart theme={null} final timeSeriesData = [ {'month': 'Jan', 'users': 1200, 'platform': 'iOS'}, {'month': 'Feb', 'users': 1350, 'platform': 'iOS'}, {'month': 'Mar', 'users': 1100, 'platform': 'iOS'}, {'month': 'Jan', 'users': 800, 'platform': 'Android'}, {'month': 'Feb', 'users': 950, 'platform': 'Android'}, {'month': 'Mar', 'users': 1200, 'platform': 'Android'}, ]; CristalyseChart() .data(timeSeriesData) .mapping(x: 'month', y: 'users', color: 'platform') .geomLine(strokeWidth: 3.0) .geomPoint(size: 6.0) .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .animate(duration: Duration(milliseconds: 1200)) .build() ``` ### Combined Line + Points Enhanced visualization with both lines and points: ```dart theme={null} CristalyseChart() .data(analyticsData) .mapping(x: 'week', y: 'engagement') .geomLine(strokeWidth: 2.0, alpha: 0.8) .geomPoint(size: 5.0, alpha: 0.9) .scaleXContinuous() .scaleYContinuous() .theme(ChartTheme.darkTheme()) .build() ``` ### Custom Category Colors Assign specific colors to categories for consistent branding and semantic meaning: ```dart theme={null} // E-commerce performance by channel final channelColors = { 'Organic Search': const Color(0xFF34D399), // Green - growth 'Paid Ads': const Color(0xFF60A5FA), // Blue - investment 'Social Media': const Color(0xFFF59E0B), // Yellow - engagement 'Email': const Color(0xFFEF4444), // Red - direct 'Referrals': const Color(0xFF8B5CF6), // Purple - partnership }; final channelData = [ {'quarter': 'Q1', 'revenue': 45000, 'channel': 'Organic Search'}, {'quarter': 'Q2', 'revenue': 52000, 'channel': 'Organic Search'}, {'quarter': 'Q1', 'revenue': 28000, 'channel': 'Paid Ads'}, {'quarter': 'Q2', 'revenue': 35000, 'channel': 'Paid Ads'}, {'quarter': 'Q1', 'revenue': 18000, 'channel': 'Social Media'}, {'quarter': 'Q2', 'revenue': 22000, 'channel': 'Social Media'}, {'quarter': 'Q1', 'revenue': 15000, 'channel': 'Email'}, {'quarter': 'Q2', 'revenue': 19000, 'channel': 'Email'}, ]; CristalyseChart() .data(channelData) .mapping(x: 'quarter', y: 'revenue', color: 'channel') .geomLine(strokeWidth: 2.5) .geomPoint(size: 6.0) .customPalette(categoryColors: channelColors) .scaleXOrdinal() .scaleYContinuous(min: 0, labels: (value) => '\$${(value/1000).toInt()}K') .build() ``` ### Server Health Monitoring Semantic colors for system status tracking: ```dart theme={null} // Infrastructure monitoring dashboard final healthColors = { 'Healthy': const Color(0xFF10B981), // Green - all good 'Warning': const Color(0xFFF59E0B), // Amber - attention needed 'Critical': const Color(0xFFEF4444), // Red - urgent 'Maintenance': const Color(0xFF6B7280), // Gray - planned downtime }; final serverData = [ {'hour': '00:00', 'count': 12, 'status': 'Healthy'}, {'hour': '06:00', 'count': 15, 'status': 'Healthy'}, {'hour': '12:00', 'count': 8, 'status': 'Healthy'}, {'hour': '00:00', 'count': 2, 'status': 'Warning'}, {'hour': '06:00', 'count': 3, 'status': 'Warning'}, {'hour': '12:00', 'count': 1, 'status': 'Warning'}, {'hour': '00:00', 'count': 0, 'status': 'Critical'}, {'hour': '06:00', 'count': 1, 'status': 'Critical'}, {'hour': '12:00', 'count': 2, 'status': 'Critical'}, ]; CristalyseChart() .data(serverData) .mapping(x: 'hour', y: 'count', color: 'status') .geomArea(strokeWidth: 1.5, alpha: 0.6) .customPalette(categoryColors: healthColors) .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.darkTheme()) .build() ``` ## Bar Charts ### Vertical Bar Chart Standard categorical data visualization: ```dart theme={null} final revenueData = [ {'quarter': 'Q1', 'revenue': 120}, {'quarter': 'Q2', 'revenue': 150}, {'quarter': 'Q3', 'revenue': 110}, {'quarter': 'Q4', 'revenue': 180}, ]; CristalyseChart() .data(revenueData) .mapping(x: 'quarter', y: 'revenue') .geomBar( width: 0.8, alpha: 0.9, borderRadius: BorderRadius.circular(4), ) .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .build() ``` ### Grouped Bar Chart Compare multiple series side-by-side: ```dart theme={null} final productData = [ {'quarter': 'Q1', 'revenue': 120, 'product': 'Widget A'}, {'quarter': 'Q2', 'revenue': 150, 'product': 'Widget A'}, {'quarter': 'Q1', 'revenue': 80, 'product': 'Widget B'}, {'quarter': 'Q2', 'revenue': 110, 'product': 'Widget B'}, ]; CristalyseChart() .data(productData) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar( style: BarStyle.grouped, width: 0.8, alpha: 0.9, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .build() ``` ### Stacked Bar Chart Show composition and totals: ```dart theme={null} final budgetData = [ {'department': 'Marketing', 'amount': 50, 'category': 'Personnel'}, {'department': 'Marketing', 'amount': 30, 'category': 'Technology'}, {'department': 'Marketing', 'amount': 20, 'category': 'Travel'}, {'department': 'Sales', 'amount': 80, 'category': 'Personnel'}, {'department': 'Sales', 'amount': 25, 'category': 'Technology'}, ]; CristalyseChart() .data(budgetData) .mapping(x: 'department', y: 'amount', color: 'category') .geomBar( style: BarStyle.stacked, width: 0.8, alpha: 0.9, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .build() ``` ### Horizontal Bar Chart Great for long category names: ```dart theme={null} CristalyseChart() .data(departmentData) .mapping(x: 'department', y: 'headcount') .geomBar( borderRadius: BorderRadius.circular(4), borderWidth: 1.0, ) .coordFlip() // Makes it horizontal .scaleXOrdinal() .scaleYContinuous(min: 0) .theme(ChartTheme.defaultTheme()) .build() ``` ## Area Charts ### Basic Area Chart Perfect for showing volume over time: ```dart theme={null} final trafficData = [ {'month': 'Jan', 'visitors': 1200}, {'month': 'Feb', 'visitors': 1350}, {'month': 'Mar', 'visitors': 1100}, {'month': 'Apr', 'visitors': 1800}, {'month': 'May', 'visitors': 1650}, ]; CristalyseChart() .data(trafficData) .mapping(x: 'month', y: 'visitors') .geomArea( strokeWidth: 2.0, alpha: 0.3, fillArea: true, color: Colors.blue, ) .scaleXOrdinal() .scaleYContinuous(min: 0) .animate(duration: Duration(milliseconds: 1200)) .build() ``` ### Multi-Series Area Chart Compare multiple categories: ```dart theme={null} final platformData = [ {'month': 'Jan', 'users': 800, 'platform': 'Mobile'}, {'month': 'Feb', 'users': 900, 'platform': 'Mobile'}, {'month': 'Jan', 'users': 400, 'platform': 'Desktop'}, {'month': 'Feb', 'users': 450, 'platform': 'Desktop'}, ]; CristalyseChart() .data(platformData) .mapping(x: 'month', y: 'users', color: 'platform') .geomArea(strokeWidth: 1.5, alpha: 0.4) .scaleXOrdinal() .scaleYContinuous(min: 0) .build() ``` ### Combined Area + Line + Points Layered visualization for rich data stories: ```dart theme={null} CristalyseChart() .data(analyticsData) .mapping(x: 'date', y: 'value') .geomArea(alpha: 0.2, strokeWidth: 0) // Background fill .geomLine(strokeWidth: 3.0) // Trend line .geomPoint(size: 6.0) // Data points .scaleXContinuous() .scaleYContinuous(min: 0) .build() ``` ## Dual Y-Axis ### Business Dashboard Revenue vs conversion rate on independent scales: ```dart theme={null} final businessData = [ {'month': 'Jan', 'revenue': 120, 'conversion_rate': 2.3}, {'month': 'Feb', 'revenue': 150, 'conversion_rate': 2.8}, {'month': 'Mar', 'revenue': 110, 'conversion_rate': 2.1}, {'month': 'Apr', 'revenue': 180, 'conversion_rate': 3.2}, ]; CristalyseChart() .data(businessData) .mapping(x: 'month', y: 'revenue') // Primary Y-axis .mappingY2('conversion_rate') // Secondary Y-axis .geomBar( yAxis: YAxis.primary, // Revenue bars (left scale) alpha: 0.7, ) .geomLine( yAxis: YAxis.secondary, // Conversion line (right scale) strokeWidth: 3.0, color: Colors.orange, ) .geomPoint( yAxis: YAxis.secondary, // Points on conversion line size: 8.0, color: Colors.orange, ) .scaleXOrdinal() .scaleYContinuous(min: 0) // Left axis: Revenue .scaleY2Continuous(min: 0, max: 5) // Right axis: Percentage .theme(ChartTheme.defaultTheme()) .build() ``` ### Sales Volume vs Customer Satisfaction ```dart theme={null} CristalyseChart() .data(salesData) .mapping(x: 'week', y: 'sales_volume') .mappingY2('satisfaction_score') .geomBar(yAxis: YAxis.primary) // Volume bars .geomLine(yAxis: YAxis.secondary) // Satisfaction trend .scaleY2Continuous(min: 1, max: 5) // Rating scale .build() ``` ## Interactive Charts ### Tooltips and Click Handlers Add rich interactivity to your charts: ```dart theme={null} CristalyseChart() .data(salesData) .mapping(x: 'week', y: 'revenue', color: 'rep') .geomPoint(size: 8.0) .interaction( tooltip: TooltipConfig( builder: (point) { final category = point.getDisplayValue('rep'); final value = point.getDisplayValue('revenue'); return Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.8), borderRadius: BorderRadius.circular(4), ), child: Text( '$category: \$${value}k', style: const TextStyle(color: Colors.white, fontSize: 12), ), ); }, ), click: ClickConfig( onTap: (point) { print('Tapped on: ${point.data}'); // Show dialog, navigate, etc. }, ), ) .build() ``` ### Panning for Large Datasets Interactive exploration of time series data: ```dart theme={null} CristalyseChart() .data(largeTimeSeriesData) .mapping(x: 'timestamp', y: 'value', color: 'series') .geomLine(strokeWidth: 2.0) .geomPoint(size: 4.0) .interaction( pan: PanConfig( enabled: true, updateXDomain: true, // Allow horizontal panning updateYDomain: false, // Disable vertical panning onPanUpdate: (info) { print('Visible range: ${info.visibleMinX} - ${info.visibleMaxX}'); // Load more data, update UI, etc. }, ), ) .build() ``` ## Theming Examples ### Dark Theme ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .theme(ChartTheme.darkTheme()) .build() ``` ### Custom Theme ```dart theme={null} final customTheme = ChartTheme( backgroundColor: Colors.grey[50]!, primaryColor: Colors.deepPurple, colorPalette: [Colors.blue, Colors.red, Colors.green], gridColor: Colors.grey[300]!, axisTextStyle: TextStyle(fontSize: 14, color: Colors.black87), padding: EdgeInsets.all(40), ); CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .theme(customTheme) .build() ``` ## Animation Examples ### Elastic Animation ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .animate( duration: Duration(milliseconds: 1200), curve: Curves.elasticOut, ) .build() ``` ### Staggered Bar Animation ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .geomBar() .animate( duration: Duration(milliseconds: 1400), curve: Curves.easeInOutCubic, ) .build() ``` Ready to build your own charts? Start with the [Quick Start Guide](/quickstart) or explore specific [Chart Types](/charts/scatter-plots)! # Animations Source: https://docs.cristalyse.com/features/animations Smooth 60fps transitions and progressive rendering ## Overview Cristalyse leverages Flutter's animation engine to deliver buttery-smooth 60fps animations. Every chart type supports customizable animations with different curves, durations, and effects. ## Basic Animation Add smooth entrance animations to any chart: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .animate( duration: Duration(milliseconds: 800), curve: Curves.easeInOut, ) .build() ``` ## Animation Curves Choose from Flutter's extensive curve library: ### Elastic Effects Perfect for playful interfaces: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .geomBar() .animate( duration: Duration(milliseconds: 1200), curve: Curves.elasticOut, ) .build() ``` ### Bounce Animation Add character to your visualizations: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint(size: 8.0) .animate( duration: Duration(milliseconds: 1000), curve: Curves.bounceOut, ) .build() ``` ### Smooth Transitions For professional dashboards: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'value') .geomLine(strokeWidth: 3.0) .animate( duration: Duration(milliseconds: 1500), curve: Curves.easeInOutCubic, ) .build() ``` ## Chart-Specific Animations ### Progressive Line Drawing Lines animate from start to finish: ```dart theme={null} CristalyseChart() .data(timeSeriesData) .mapping(x: 'date', y: 'price') .geomLine(strokeWidth: 2.0) .animate(duration: Duration(milliseconds: 2000)) .build() ``` ### Staggered Bar Growth Bars animate individually with delays: ```dart theme={null} CristalyseChart() .data(categoryData) .mapping(x: 'category', y: 'value') .geomBar() .animate( duration: Duration(milliseconds: 1400), curve: Curves.easeInOutBack, ) .build() ``` ### Point Emergence Scatter plot points appear progressively: ```dart theme={null} CristalyseChart() .data(scatterData) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint(size: 6.0, alpha: 0.8) .animate( duration: Duration(milliseconds: 800), curve: Curves.elasticOut, ) .build() ``` ### Area Fill Animation Area charts fill progressively: ```dart theme={null} CristalyseChart() .data(volumeData) .mapping(x: 'month', y: 'volume') .geomArea(alpha: 0.3, strokeWidth: 2.0) .animate( duration: Duration(milliseconds: 1600), curve: Curves.easeInOutCubic, ) .build() ``` ## Advanced Animation Techniques ### Layered Animations Combine multiple geometries with different timings: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'week', y: 'engagement') .geomArea(alpha: 0.2, strokeWidth: 0) // Background area .geomLine(strokeWidth: 3.0) // Trend line .geomPoint(size: 6.0) // Data points .animate( duration: Duration(milliseconds: 1800), curve: Curves.easeInOutCubic, ) .build() ``` ### Stacked Bar Segments Each segment animates individually: ```dart theme={null} CristalyseChart() .data(stackedData) .mapping(x: 'quarter', y: 'revenue', color: 'category') .geomBar(style: BarStyle.stacked) .animate( duration: Duration(milliseconds: 1600), curve: Curves.easeInOutQuart, ) .build() ``` ### Dual Axis Coordination Synchronize animations across different Y-axes: ```dart theme={null} CristalyseChart() .data(businessData) .mapping(x: 'month', y: 'revenue') .mappingY2('conversion_rate') .geomBar(yAxis: YAxis.primary) .geomLine(yAxis: YAxis.secondary, strokeWidth: 3.0) .animate( duration: Duration(milliseconds: 1400), curve: Curves.easeInOutCubic, ) .build() ``` ## Animation Curves Reference * `Curves.easeIn` - Slow start, fast finish * `Curves.easeOut` - Fast start, slow finish * `Curves.easeInOut` - Slow start and finish * `Curves.easeInOutCubic` - Smooth S-curve * `Curves.elasticIn` - Spring compression * `Curves.elasticOut` - Spring release * `Curves.bounceIn` - Bouncing entrance * `Curves.bounceOut` - Bouncing exit * `Curves.easeInBack` - Slight overshoot at start * `Curves.easeOutBack` - Slight overshoot at end * `Curves.easeInOutBack` - Overshoot both ends * `Curves.fastOutSlowIn` - Material Design standard * `Curves.slowMiddle` - Fast start/end, slow middle * `Curves.decelerate` - Quick deceleration ## Performance Optimization ### Duration Guidelines Choose appropriate durations for different chart types: * **Scatter plots**: 600-1000ms * **Line charts**: 1000-2000ms * **Bar charts**: 800-1400ms * **Area charts**: 1200-1800ms * **Complex dual-axis**: 1400-2200ms ### Memory Efficiency Cristalyse animations are optimized for: * **GPU acceleration** - Leverages Flutter's rendering pipeline * **Minimal redraws** - Only animates changing elements * **Smooth 60fps** - Consistent frame rates across devices * **Memory efficient** - Automatic cleanup after animation ## Interactive Animation ### Data Updates Smooth transitions when data changes: ```dart theme={null} class AnimatedDashboard extends StatefulWidget { @override _AnimatedDashboardState createState() => _AnimatedDashboardState(); } class _AnimatedDashboardState extends State { List> data = initialData; void updateData() { setState(() { data = newData; // Chart automatically animates to new data }); } @override Widget build(BuildContext context) { return CristalyseChart() .data(data) .mapping(x: 'month', y: 'value') .geomLine(strokeWidth: 3.0) .animate(duration: Duration(milliseconds: 800)) .build(); } } ``` ### Theme Transitions Smooth theme changes: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .theme(isDark ? ChartTheme.darkTheme() : ChartTheme.defaultTheme()) .animate(duration: Duration(milliseconds: 600)) .build() ``` ## Best Practices * Short animations (300-600ms) for simple interactions * Medium animations (800-1400ms) for chart rendering * Long animations (1500-2500ms) for complex visualizations * Avoid animations longer than 3 seconds * Use `easeInOutCubic` for professional dashboards * Use `elasticOut` for engaging presentations * Use `fastOutSlowIn` for Material Design consistency * Avoid jarring curves like `bounceIn` for serious data * Test on slower devices to ensure 60fps * Consider reducing animation complexity for large datasets * Use `shouldRepaint` optimization for static elements * Profile animation performance during development * Respect user's reduced motion preferences * Provide option to disable animations * Ensure animations don't interfere with screen readers * Use appropriate duration for cognitive accessibility ## Animation Showcase
Progressive Line Drawing Staggered Bar Growth
Point Emergence Stacked Animation
Various animation types: Progressive line drawing, staggered bars, point emergence, and stacked segments
Lines draw smoothly from start to finish, perfect for revealing trends Bars grow individually with delays, creating engaging sequences Scatter plot points appear progressively with elastic effects Areas fill smoothly with coordinated stroke and fill animations ## Next Steps Customize visual styles and color schemes Add tooltips, hover effects, and user interactions Optimize charts for large datasets and smooth rendering Export animated charts as high-quality graphics # Export Source: https://docs.cristalyse.com/features/export Export charts as high-quality vector graphics and images ## Overview Cristalyse provides powerful export capabilities, allowing you to save charts as scalable vector graphics (SVG) for use in presentations, reports, and publications. All visual elements, styling, and data are preserved in the exported format. ## SVG Export ### Basic SVG Export Export charts as scalable vector graphics with perfect quality at any size: ```dart theme={null} // Create your chart final chart = CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue') .geomBar() .theme(ChartTheme.defaultTheme()) .build(); // Export as SVG final result = await chart.exportAsSvg( width: 800, height: 600, filename: 'revenue_chart', ); print('Chart exported to: ${result.filePath}'); ``` ### Advanced Export Configuration Customize export settings for different use cases: ```dart theme={null} final exportConfig = ExportConfig( width: 1920, // High resolution for presentations height: 1080, format: ExportFormat.svg, backgroundColor: Colors.white, filename: 'dashboard_chart', quality: 1.0, // Maximum quality transparentBackground: false, ); final result = await chart.export(exportConfig); ``` ### Export with Custom Dimensions Optimize for different output formats: ```dart theme={null} // For presentations (16:9 aspect ratio) await chart.exportAsSvg( width: 1920, height: 1080, filename: 'presentation_chart', ); // For print documents (300 DPI equivalent) await chart.exportAsSvg( width: 2400, height: 1800, filename: 'print_chart', ); // For web thumbnails await chart.exportAsSvg( width: 400, height: 300, filename: 'thumbnail_chart', ); ``` ## Export Examples ### Dashboard Export Export complex multi-chart dashboards: ```dart theme={null} class DashboardExporter { static Future> exportDashboard( List charts, ) async { final results = []; for (int i = 0; i < charts.length; i++) { final result = await charts[i].exportAsSvg( width: 800, height: 600, filename: 'dashboard_chart_$i', ); results.add(result); } return results; } } // Usage final charts = [revenueChart, usersChart, conversionChart]; final exportResults = await DashboardExporter.exportDashboard(charts); for (final result in exportResults) { print('Exported: ${result.filePath} (${result.fileSizeBytes} bytes)'); } ``` ### Themed Export Export charts with consistent branding: ```dart theme={null} class BrandedExporter { static final brandTheme = ChartTheme( backgroundColor: const Color(0xFFF8F9FA), primaryColor: const Color(0xFF007ACC), colorPalette: [ const Color(0xFF007ACC), const Color(0xFFFF6B35), const Color(0xFF28A745), const Color(0xFFDC3545), ], // ... other brand properties ); static Future exportBrandedChart({ required List> data, required String xColumn, required String yColumn, required String filename, }) async { final chart = CristalyseChart() .data(data) .mapping(x: xColumn, y: yColumn) .geomBar() .theme(brandTheme) .build(); return await chart.exportAsSvg( width: 1200, height: 800, backgroundColor: brandTheme.backgroundColor, filename: filename, ); } } ``` ### Batch Export Export multiple chart variations efficiently: ```dart theme={null} class BatchExporter { static Future exportChartVariations({ required List> data, required String baseFilename, }) async { final themes = [ ChartTheme.defaultTheme(), ChartTheme.darkTheme(), ChartTheme.solarizedLightTheme(), ]; final themeNames = ['light', 'dark', 'solarized']; for (int i = 0; i < themes.length; i++) { final chart = CristalyseChart() .data(data) .mapping(x: 'month', y: 'value') .geomLine(strokeWidth: 3.0) .geomPoint(size: 6.0) .theme(themes[i]) .build(); await chart.exportAsSvg( width: 800, height: 600, filename: '${baseFilename}_${themeNames[i]}', ); } } } ``` ## Export Quality & Optimization ### Vector Benefits SVG exports provide numerous advantages: * **Infinite Scalability**: Perfect quality at any size * **Small File Sizes**: Efficient vector representation * **Editable**: Can be modified in vector graphics software * **Web Ready**: Direct embedding in HTML * **Print Quality**: Crisp output at any resolution ### File Size Optimization Control output file size for different use cases: ```dart theme={null} // Minimal file size for web await chart.exportAsSvg( width: 600, height: 400, filename: 'web_optimized', ); // High quality for print await chart.exportAsSvg( width: 2400, height: 1600, filename: 'print_quality', ); ``` ### Performance Considerations Optimize export performance for large datasets: ```dart theme={null} class PerformantExporter { static Future exportLargeDataset({ required List> data, required String filename, }) async { // Simplify data for export if needed final exportData = data.length > 1000 ? _downsampleData(data, 1000) : data; final chart = CristalyseChart() .data(exportData) .mapping(x: 'x', y: 'y') .geomLine(strokeWidth: 2.0) .build(); return await chart.exportAsSvg( width: 1200, height: 800, filename: filename, ); } static List> _downsampleData( List> data, int targetCount, ) { final step = data.length / targetCount; final result = >[]; for (int i = 0; i < data.length; i += step.ceil()) { result.add(data[i]); } return result; } } ``` ## Export Integration ### File Management Handle exported files efficiently: ```dart theme={null} class ExportManager { static const String exportDirectory = 'chart_exports'; static Future getExportDirectory() async { final documentsDir = await getApplicationDocumentsDirectory(); final exportDir = Directory('${documentsDir.path}/$exportDirectory'); if (!await exportDir.exists()) { await exportDir.create(recursive: true); } return exportDir; } static Future exportWithTimestamp({ required CristalyseChart chart, required String baseName, }) async { final timestamp = DateTime.now().millisecondsSinceEpoch; final filename = '${baseName}_$timestamp'; final exportDir = await getExportDirectory(); final customPath = '${exportDir.path}/$filename.svg'; return await chart.exportAsSvg( width: 1200, height: 800, filename: filename, customPath: customPath, ); } } ``` ### Share Integration Export and share charts directly: ```dart theme={null} class ShareableExporter { static Future exportAndShare({ required CristalyseChart chart, required String title, required BuildContext context, }) async { try { // Export chart final result = await chart.exportAsSvg( width: 1200, height: 800, filename: 'shared_chart', ); // Share the exported file await Share.shareFiles( [result.filePath], text: title, subject: 'Chart Export', ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Export failed: $e'), backgroundColor: Colors.red, ), ); } } } ``` ### Cloud Storage Integration Upload exports to cloud storage: ```dart theme={null} class CloudExporter { static Future exportToCloud({ required CristalyseChart chart, required String cloudPath, }) async { // Export locally first final result = await chart.exportAsSvg( width: 1200, height: 800, filename: 'temp_export', ); // Upload to cloud storage (example with Firebase) final file = File(result.filePath); final storageRef = FirebaseStorage.instance.ref().child(cloudPath); final uploadTask = await storageRef.putFile(file); final downloadUrl = await uploadTask.ref.getDownloadURL(); // Clean up local file await file.delete(); return downloadUrl; } } ``` ## Error Handling ### Robust Export Operations Handle export failures gracefully: ```dart theme={null} class SafeExporter { static Future safeExport({ required CristalyseChart chart, required String filename, required VoidCallback? onSuccess, required Function(String)? onError, }) async { try { final result = await chart.exportAsSvg( width: 1200, height: 800, filename: filename, ); onSuccess?.call(); return result; } on ChartExportException catch (e) { onError?.call('Export failed: ${e.message}'); return null; } catch (e) { onError?.call('Unexpected error: $e'); return null; } } } // Usage await SafeExporter.safeExport( chart: myChart, filename: 'report_chart', onSuccess: () => showSuccessMessage(), onError: (error) => showErrorDialog(error), ); ``` ### Export Validation Verify export results: ```dart theme={null} class ExportValidator { static bool validateExportResult(ExportResult result) { // Check file exists final file = File(result.filePath); if (!file.existsSync()) return false; // Check file size is reasonable if (result.fileSizeBytes < 100) return false; // Too small if (result.fileSizeBytes > 10 * 1024 * 1024) return false; // Too large // Check dimensions if (result.dimensions.width <= 0 || result.dimensions.height <= 0) { return false; } return true; } } ``` ## Export Formats & Use Cases **Best for:** * Presentations and reports * Web embedding * Print publications * Scalable graphics **Features:** * Infinite scalability * Small file sizes * Editable in vector software * Perfect text rendering **Dimensions:** * 4K: 3840×2160 * Print: 300 DPI equivalent * Poster: 5000×3000+ **Applications:** * Large format printing * High-DPI displays * Professional publications **Specifications:** * Standard: 800×600 * Thumbnail: 400×300 * Banner: 1200×400 **Benefits:** * Fast loading * Responsive design * SEO friendly ## Best Practices * Use descriptive filenames with timestamps * Organize exports in dedicated directories * Include chart metadata in filenames * Implement cleanup routines for old exports * Validate export results before use * Test exports at different scales * Verify color accuracy across formats * Check text readability at target sizes * Batch exports for efficiency * Downsample large datasets when appropriate * Use appropriate dimensions for use case * Clean up temporary files promptly * Provide progress indicators for long exports * Show clear success/failure messages * Offer preview before final export * Enable easy sharing and storage options ## Export Examples High-resolution exports optimized for professional presentations Lightweight SVG files perfect for web embedding High-DPI exports suitable for professional printing Efficient batch export workflows for multiple charts ## Next Steps Export charts with consistent branding and styling Preserve interactive elements in static exports Optimize export performance for large datasets Create export-optimized theme variations # Interactions Source: https://docs.cristalyse.com/features/interactions Add tooltips, hover effects, clicks, panning, and zooming to your charts ## Overview Cristalyse provides rich interaction capabilities that make your charts engaging and informative. From simple tooltips to complex pan and zoom operations, every interaction is optimized for performance and accessibility. Support for pinch gestures, scroll wheel, and floating buttons brings intuitive zooming to all platforms. ## Tooltip Interactions ### Basic Tooltips Show contextual information when users hover over data points: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue') .geomPoint() .tooltip(DefaultTooltips.simple('revenue')) .build() ``` ### Multi-Column Tooltips Display multiple data fields in formatted tooltips: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'week', y: 'sales', color: 'region') .geomBar() .tooltip(DefaultTooltips.multi({ 'week': 'Week', 'sales': 'Sales ($)', 'region': 'Region', 'growth': 'Growth Rate', })) .build() ``` ### Custom Tooltip Builders Create rich, branded tooltips with custom styling: ```dart theme={null} 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() ``` ### Tooltip Configuration Fine-tune tooltip behavior and appearance: ```dart theme={null} 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() ``` ## Hover Interactions ### Basic Hover Detection Respond to mouse hover events on data points: ```dart theme={null} 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() ``` ### Advanced Hover Configuration Control hover sensitivity and debouncing: ```dart theme={null} 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() ``` ## Click Interactions ### Simple Click Handlers Handle tap events on data points: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue') .geomBar() .onClick((point) { Navigator.push( context, MaterialPageRoute( builder: (context) => DetailPage(data: point.data), ), ); }) .build() ``` ### Multiple Click Types Support different interaction patterns: ```dart theme={null} 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() ``` ## Pan Interactions ### Basic Panning Enable horizontal scrolling through large datasets: ```dart theme={null} 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() ``` ### Advanced Pan Configuration Full control over pan behavior: ```dart theme={null} 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() ``` ### Programmatic Pan Control Control chart panning programmatically with external UI controls: ```dart theme={null} class ProgrammaticPanChart extends StatefulWidget { @override _ProgrammaticPanChartState createState() => _ProgrammaticPanChartState(); } class _ProgrammaticPanChartState extends State { final panController = PanController(); double visibleMinX = 500; double visibleMaxX = 1500; @override void dispose() { panController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Column( children: [ // External controls Row( children: [ ElevatedButton( onPressed: () { panController.panTo(PanInfo( visibleMinX: visibleMinX, visibleMaxX: visibleMaxX, state: PanState.update, )); }, child: Text('Pan to Range'), ), ElevatedButton( onPressed: () => panController.panReset(), child: Text('Reset View'), ), ], ), // Chart with controller Expanded( child: CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomLine() .interaction( pan: PanConfig( enabled: true, controller: panController, // Connect controller onPanUpdate: (info) { setState(() { visibleMinX = info.visibleMinX ?? visibleMinX; visibleMaxX = info.visibleMaxX ?? visibleMaxX; }); }, ), ) .build(), ), ], ); } } ``` **Key Features:** * `panTo()`: Jump to specific data range programmatically * `panReset()`: Return to original chart view * Works alongside gesture-based panning * Full lifecycle management with dispose() **Use Cases:** * Zoom buttons for specific time ranges ("Last 7 days", "Last month") * Coordinated panning across multiple charts * Jump to bookmarked positions * Reset button after exploration * External slider controls for pan position ### Zoom Interactions Enable intuitive zooming with pinch gestures, scroll wheel, and UI buttons: #### Basic Zoom Setup Quick zoom configuration with one method: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'day', y: 'revenue') .geomLine() .onZoom((info) { print('Zoom scale: ${info.scaleX}x'); print('Visible range: ${info.visibleMinX} to ${info.visibleMaxX}'); }) .build() ``` #### Zoom Modes Zoom can be configured for different axis combinations: ```dart theme={null} // X-axis only (default) CristalyseChart() .data(data) .mapping(x: 'time', y: 'value') .geomLine() .onZoom( (info) => print('X zoom: ${info.scaleX}x'), axis: ZoomAxis.x, ) .build() // Y-axis only CristalyseChart() .data(data) .mapping(x: 'time', y: 'value') .geomLine() .onZoom( (info) => print('Y zoom: ${info.scaleY}x'), axis: ZoomAxis.y, ) .build() // Both axes simultaneously CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomPoint() .onZoom( (info) => print('Scale X: ${info.scaleX}x, Y: ${info.scaleY}x'), axis: ZoomAxis.both, ) .build() ``` #### Advanced Zoom Configuration Full control over zoom behavior and appearance: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'timestamp', y: 'metric', color: 'series') .geomLine(strokeWidth: 2.0) .geomPoint() .interaction( zoom: ZoomConfig( enabled: true, axes: ZoomAxis.both, // Zoom X and Y axes maxScale: 16.0, // Maximum 16x zoom minScale: 1.0, // Minimum 1x (no zoom out past original) wheelSensitivity: 0.0015, // Scroll wheel sensitivity (0.0005-0.0035) buttonStep: 1.4, // Zoom 40% per button press showButtons: true, // Show +/- floating buttons buttonAlignment: Alignment.bottomRight, // Button position buttonPadding: const EdgeInsets.all(20), // Space from chart edge onZoomStart: (info) { print('Zoom started at ${info.visibleMinX}'); }, onZoomUpdate: (info) { print('Zooming - Scale: ${info.scaleX}x'); // Update UI in real-time setState(() { currentZoomLevel = info.scaleX ?? 1.0; visibleRange = '${info.visibleMinX?.toStringAsFixed(2)} - ${info.visibleMaxX?.toStringAsFixed(2)}'; }); }, onZoomEnd: (info) { print('Zoom ended'); // Fetch new data for visible range if needed if (info.visibleMinX != null && info.visibleMaxX != null) { loadDataForRange(info.visibleMinX!, info.visibleMaxX!); } }, ), ) .build() ``` #### Zoom Input Methods **Pinch Gesture (Touch):** * Two-finger pinch to zoom in/out * Zoom centered at pinch focal point * Works with configured axes **Scroll Wheel (Desktop):** * Scroll up to zoom in, scroll down to zoom out * Zoom centered at cursor position * Configurable sensitivity with `wheelSensitivity` * Precise zoom control for time-series and technical charts **Floating Buttons (All Platforms):** * Optional +/- buttons for touch-friendly zoom * Configurable positioning with `buttonAlignment` * Adjustable zoom increment with `buttonStep` * Accessible for users with limited touch capability **Single Finger (Touch):** * Single finger performs pan gesture * Works alongside zoom when multi-touch is detected #### Zoom Information The `ZoomInfo` object provides comprehensive zoom state data: ```dart theme={null} ZoomInfo( // Visible axis ranges in data coordinates visibleMinX: 100.0, visibleMaxX: 500.0, visibleMinY: 10.0, visibleMaxY: 90.0, // Zoom scale factors (1.0 = no zoom, 4.0 = 4x zoom) scaleX: 2.0, // 2x zoom on X-axis scaleY: 1.5, // 1.5x zoom on Y-axis // Current zoom lifecycle state state: ZoomState.update, // start, update, or end ) ``` Use `ZoomState` to differentiate interaction phases: * `ZoomState.start` - User begins zoom gesture * `ZoomState.update` - Zoom in progress (fires continuously) * `ZoomState.end` - User completes zoom gesture #### Combined Zoom & Pan Zoom and pan work seamlessly together: ```dart theme={null} CristalyseChart() .data(timeSeriesData) .mapping(x: 'timestamp', y: 'value', color: 'category') .geomLine(strokeWidth: 2.0) .geomPoint(size: 5.0) .interaction( zoom: ZoomConfig( enabled: true, axes: ZoomAxis.x, // Zoom on X-axis only (time dimension) wheelSensitivity: 0.0015, showButtons: true, onZoomUpdate: (info) { setState(() { zoomDisplay = 'Zoom: ${info.scaleX?.toStringAsFixed(1)}x • ' 'Range: ${info.visibleMinX?.toStringAsFixed(0)} - ${info.visibleMaxX?.toStringAsFixed(0)}'; }); }, ), pan: PanConfig( enabled: true, updateXDomain: true, // Pan X-axis updateYDomain: false, // Keep Y-axis fixed throttle: const Duration(milliseconds: 16), onPanUpdate: (info) => print('Panning to ${info.visibleMinX}'), ), tooltip: TooltipConfig( builder: DefaultTooltips.multi({ 'timestamp': 'Time', 'value': 'Value', 'category': 'Category', }), ), ) .build() ``` #### Use Cases **Time-Series Exploration:** ```dart theme={null} // Users can zoom into specific time periods // then pan to explore surrounding data CristalyseChart() .data(stockData) .mapping(x: 'date', y: 'price') .geomLine() .interaction( zoom: ZoomConfig( axes: ZoomAxis.x, showButtons: true, // Easy zoom on mobile ), pan: PanConfig(enabled: true, updateXDomain: true), ) .build() ``` **Scientific Visualizations:** ```dart theme={null} // Independent X/Y zooming for scatter plots CristalyseChart() .data(measurementData) .mapping(x: 'temperature', y: 'pressure', color: 'material') .geomPoint(size: 8.0) .interaction( zoom: ZoomConfig( axes: ZoomAxis.both, // Zoom both axes independently maxScale: 32.0, // Allow deep zoom for detailed analysis ), ) .build() ``` **Mobile Dashboards:** ```dart theme={null} // Touch-friendly zoom buttons for accessibility CristalyseChart() .data(mobileData) .mapping(x: 'hour', y: 'users') .geomBar() .interaction( zoom: ZoomConfig( axes: ZoomAxis.x, showButtons: true, buttonAlignment: Alignment.topRight, // Accessible location buttonStep: 1.5, // Larger steps for mobile ), ) .build() ``` ### Pan with Data Loading Implement infinite scrolling patterns: ```dart theme={null} class PanningChart extends StatefulWidget { @override _PanningChartState createState() => _PanningChartState(); } class _PanningChartState extends State { List> 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(); } } ``` ## Combined Interactions ### Rich Interactive Dashboard Combine multiple interaction types for powerful user experience: ```dart theme={null} 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() ``` ## Performance Optimization ### Large Dataset Interactions Optimize interactions for thousands of data points: ```dart theme={null} 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() ``` ### Memory-Efficient Tooltips Reuse tooltip widgets to reduce memory allocation: ```dart theme={null} class PerformantTooltips { static final _tooltipPool = []; 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() ``` ## Accessibility ### Screen Reader Support Ensure interactions work with assistive technologies: ```dart theme={null} 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() ``` ### Keyboard Navigation Support keyboard-only users: ```dart theme={null} class KeyboardNavigableChart extends StatefulWidget { @override _KeyboardNavigableChartState createState() => _KeyboardNavigableChartState(); } class _KeyboardNavigableChartState extends State { 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(), ); } } ``` ## Best Practices * Use generous hit test radii (15-30px) for touch devices * Smaller hit areas (8-15px) for mouse-only interfaces * Consider overlapping points in dense visualizations * Test on actual devices for optimal sizing * Debounce rapid interactions to prevent performance issues * Use throttling for pan operations (16-32ms intervals) * Zoom updates are throttled internally and cannot be configured * Optimize tooltip rendering for large datasets * Consider disabling interactions during animations * Provide immediate visual feedback for all interactions * Use consistent interaction patterns across charts * Show loading states during data fetch operations * Implement undo/redo for destructive actions * Support keyboard navigation where appropriate * Provide alternative text for screen readers * Ensure sufficient color contrast in tooltips * Test with assistive technologies ## Interaction Examples Multi-column tooltips with custom styling and animations Navigation, details, and context menus triggered by clicks Pinch, scroll wheel, and button-based zoom for all platforms Explore large datasets with smooth panning gestures Visual highlights and data previews on mouse hover ## Next Steps Combine interactions with smooth animations Style interaction elements to match your design Optimize interactions for large datasets Export charts while preserving interaction data # Built-in Legends Source: https://docs.cristalyse.com/features/legends Add professional legends to your charts with automatic positioning and dark mode support ## Overview Cristalyse provides built-in legend support that automatically generates beautiful legends from your chart's color mapping data. With just `.legend()`, you get intelligent symbol generation, smart positioning, and full dark mode compatibility. **New in v1.5.0**: Built-in legend support with zero configuration required! ## Quick Start Add a legend to any chart with color mapping in just one line: ```dart theme={null} CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar(style: BarStyle.grouped) .legend() // ✨ That's it! Auto-positioned legend with product categories .build(); ``` ## Key Features Works automatically with smart defaults - just add `.legend()` Flexible positioning: corners, edges, floating with custom coordinates Automatically matches your chart type: squares, circles, lines Legend text adapts automatically to light and dark themes ## Positioning ### Basic Positioning Control legend placement with the `position` parameter: ```dart theme={null} // Bottom legend (horizontal layout) .legend(position: LegendPosition.bottom) // Right side legend (vertical layout) .legend(position: LegendPosition.right) // Top-right corner (default) .legend(position: LegendPosition.topRight) ``` ### All Position Options * `LegendPosition.topLeft` * `LegendPosition.topRight` (default) * `LegendPosition.bottomLeft` * `LegendPosition.bottomRight` * `LegendPosition.top` * `LegendPosition.bottom` * `LegendPosition.left` * `LegendPosition.right` * `LegendPosition.floating` * Set custom coordinates with `floatingOffset` * Perfect for overlay layouts ### Visual Examples ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'sales', color: 'region') .geomBar() .legend() // Default: topRight position .build(); ``` Legend appears in the top-right corner with vertical layout. ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'sales', color: 'region') .geomBar() .legend(position: LegendPosition.bottom) .build(); ``` Legend appears at bottom with horizontal layout, perfect for mobile. ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'sales', color: 'region') .geomBar() .legend(position: LegendPosition.right) .build(); ``` Legend appears on the right side with vertical layout. ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'sales', color: 'region') .geomBar() .legend( position: LegendPosition.floating, floatingOffset: Offset(100, 30), // x: 100, y: 30 from top-left ) .build(); ``` Legend floats at specified coordinates with absolute positioning. ## Floating Legends **New Feature**: Floating legends give you complete control over legend positioning with pixel-perfect placement! ### Basic Floating Legend Use floating position for custom positioning: ```dart theme={null} CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar() .legend( position: LegendPosition.floating, floatingOffset: Offset(50, 20), // x: 50px, y: 20px from top-left ) .build(); ``` ### Styled Floating Legend Combine floating position with custom styling: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'date', y: 'value', color: 'metric') .geomLine() .legend( position: LegendPosition.floating, floatingOffset: Offset(120, 40), backgroundColor: Colors.white.withValues(alpha: 0.95), textStyle: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), symbolSize: 14.0, itemSpacing: 10.0, borderRadius: 8.0, ) .build(); ``` ### Use Cases for Floating Legends Perfect for complex dashboards where legends need to float over chart content without taking up layout space. ```dart theme={null} .legend( position: LegendPosition.floating, floatingOffset: Offset(200, 50), backgroundColor: Colors.white.withValues(alpha: 0.9), ) ``` When you need precise control over legend placement in complex multi-chart layouts. ```dart theme={null} .legend( position: LegendPosition.floating, floatingOffset: Offset(chartWidth - 150, 30), ) ``` Programmatically adjust floating position based on screen size or device. ```dart theme={null} final offset = isMobile ? Offset(20, 20) : Offset(100, 50); .legend( position: LegendPosition.floating, floatingOffset: offset, ) ``` ### Floating Legend Parameters | Parameter | Type | Description | | ------------------- | ------------------------- | ------------------------------------------------------------ | | `position` | `LegendPosition.floating` | Required - enables floating mode | | `floatingOffset` | `Offset?` | Coordinates from top-left corner. Default: `Offset(16, 16)` | | `floatingDraggable` | `bool` | Enable drag-to-reposition (future feature). Default: `false` | **Coordinate System**: The `floatingOffset` uses the Flutter coordinate system where `Offset(x, y)` positions the legend `x` pixels from the left edge and `y` pixels from the top edge. ## Interactive Legends **New Feature**: Interactive legends allow users to click legend items to show/hide data categories! ### Basic Interactive Legend Enable click-to-toggle with one parameter: ```dart theme={null} CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar(style: BarStyle.grouped) .legend(interactive: true) // ✨ Click to toggle visibility .build(); ``` **Features**: * **Click legend items** to show/hide categories * **Visual feedback**: Hidden items appear dimmed with strikethrough * **Smooth animations**: Data fades in/out gracefully * **Auto-managed state**: No additional code needed ### External State Management For advanced control, manage hidden categories externally: ```dart theme={null} class MyChart extends StatefulWidget { @override State createState() => _MyChartState(); } class _MyChartState extends State { final Set hiddenCategories = {}; @override Widget build(BuildContext context) { return CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar() .legend( interactive: true, hiddenCategories: hiddenCategories, onToggle: (category, visible) { setState(() { if (visible) { hiddenCategories.remove(category); } else { hiddenCategories.add(category); } }); print('$category is now ${visible ? "visible" : "hidden"}'); }, ) .build(); } } ``` ### Interactive Legend Parameters | Parameter | Type | Description | | ------------------ | ------------------------- | ----------------------------------------------------------- | | `interactive` | `bool` | Enable click-to-toggle functionality. Default: `false` | | `hiddenCategories` | `Set?` | Externally managed set of hidden categories. Optional | | `onToggle` | `Function(String, bool)?` | Callback when legend item is toggled. `(category, visible)` | ### Use Cases Users can focus on specific categories by hiding others, making complex charts easier to analyze. Quickly compare subsets of data by toggling relevant categories on and off. Interactively show/hide data during presentations to highlight key insights. Save user's hidden categories to provide personalized dashboard views. ### Interactive with Floating Position Combine interactive and floating for maximum flexibility: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'date', y: 'value', color: 'metric') .geomLine() .legend( position: LegendPosition.floating, floatingOffset: Offset(120, 40), interactive: true, // Click-to-toggle enabled backgroundColor: Colors.white.withValues(alpha: 0.95), borderRadius: 8.0, ) .build(); ``` **Performance**: Data filtering is efficient and only rebuilds the chart when categories are toggled. ## Symbol Generation Legends automatically choose appropriate symbols based on your chart type: | Chart Type | Symbol | Description | | ----------------- | -------- | --------------------------------------------- | | **Bar Charts** | ◼ Square | Solid colored squares for categorical bars | | **Line Charts** | ━ Line | Horizontal line segments matching chart lines | | **Scatter Plots** | ● Circle | Circular symbols for point data | | **Area Charts** | ━ Line | Line symbols representing area boundaries | | **Bubble Charts** | ● Circle | Circular symbols for bubble data | Symbol selection is automatic but can be overridden with custom styling if needed. ## Dark Mode Support Legend text automatically adapts to your chart theme: ```dart Light Theme theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'sales', color: 'product') .geomBar() .theme(ChartTheme.defaultTheme()) // Light theme .legend() // Dark text on light background .build(); ``` ```dart Dark Theme theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'sales', color: 'product') .geomBar() .theme(ChartTheme.darkTheme()) // Dark theme .legend() // Light text on dark background .build(); ``` **Theme Inheritance**: Legend text uses `theme.axisColor` for perfect contrast in any theme. ## Custom Styling ### Background and Appearance Add custom styling for professional dashboards: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar() .legend( position: LegendPosition.right, backgroundColor: Colors.white.withValues(alpha: 0.95), borderRadius: 8.0, symbolSize: 14.0, itemSpacing: 12.0, ) .build(); ``` ### Text Styling Customize legend text while preserving theme colors: ```dart theme={null} .legend( textStyle: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, // Color automatically inherits from theme ), symbolSize: 16.0, ) ``` **Theme Colors**: Avoid hardcoding text colors. Let the legend inherit from your theme for proper dark mode support. ### All Styling Options | Parameter | Type | Default | Description | | ----------------- | ---------------- | ---------- | ---------------------------------------- | | `position` | `LegendPosition` | `topRight` | Legend placement | | `backgroundColor` | `Color?` | `null` | Legend background color | | `textStyle` | `TextStyle?` | `null` | Text styling (inherits color from theme) | | `symbolSize` | `double` | `12.0` | Size of legend symbols | | `itemSpacing` | `double` | `8.0` | Space between legend items | | `spacing` | `double` | `12.0` | Space between legend and chart | | `borderRadius` | `double` | `4.0` | Background border radius | ## Real-World Examples ### Dashboard Analytics Perfect for executive dashboards with multiple data series: ```dart theme={null} CristalyseChart() .data(regionalSales) .mapping(x: 'month', y: 'revenue', color: 'region') .geomLine(strokeWidth: 3.0) .geomPoint(size: 6.0) .legend( position: LegendPosition.bottom, backgroundColor: Colors.grey.shade50, borderRadius: 12.0, ) .build(); ``` ### Mobile-Optimized Bottom legends work great on mobile devices: ```dart theme={null} CristalyseChart() .data(performanceData) .mapping(x: 'date', y: 'value', color: 'metric') .geomArea(alpha: 0.7) .legend(position: LegendPosition.bottom) // Mobile-friendly .build(); ``` ### Dark Theme Dashboard Professional dark mode with custom styling: ```dart theme={null} CristalyseChart() .data(analyticsData) .mapping(x: 'week', y: 'users', color: 'platform') .geomBar(style: BarStyle.grouped) .theme(ChartTheme.darkTheme()) .legend( position: LegendPosition.topRight, backgroundColor: Colors.black.withValues(alpha: 0.7), borderRadius: 8.0, symbolSize: 14.0, ) .build(); ``` ## Best Practices * Use `LegendPosition.bottom` for mobile layouts * Keep symbol sizes readable on small screens * Consider horizontal orientation for many categories * Let legends inherit theme colors automatically * Use subtle backgrounds with transparency * Maintain consistent spacing across charts * Ensure category names are descriptive * Limit legend items to 6-8 for readability * Position legends to not obscure important data * Rely on automatic theme colors for contrast * Test legends in both light and dark modes * Ensure sufficient color differentiation ## Advanced Usage ### Multi-Series Line Charts Perfect for time-series data with multiple metrics: ```dart theme={null} CristalyseChart() .data(timeSeriesData) .mapping(x: 'date', y: 'value', color: 'metric') .geomLine(strokeWidth: 2.5) .scaleXContinuous() .scaleYContinuous( labels: (value) => '${value.toStringAsFixed(1)}K', ) .legend(position: LegendPosition.topLeft) .build(); ``` ### Grouped Bar Charts with Custom Colors Combine custom palettes with legends: ```dart theme={null} final brandColors = { 'iOS': const Color(0xFF007ACC), 'Android': const Color(0xFF3DDC84), 'Web': const Color(0xFFFF6B35), }; CristalyseChart() .data(platformData) .mapping(x: 'quarter', y: 'users', color: 'platform') .geomBar(style: BarStyle.grouped) .customPalette(categoryColors: brandColors) .legend(position: LegendPosition.right) .build(); ``` ## Requirements **Color Mapping Required**: Legends only work when you have a `color` parameter in your `.mapping()` call. ```dart theme={null} // Works - has color mapping .mapping(x: 'month', y: 'sales', color: 'product') .legend() // Won't show - no color mapping .mapping(x: 'month', y: 'sales') .legend() // Legend will be empty ``` ## Migration Guide ### From Manual Legends If you were manually creating legends, you can now use the built-in functionality: ```dart Before (Manual) theme={null} // Manual legend creation with custom widgets Column( children: [ Row( children: [ Container(width: 12, height: 12, color: Colors.blue), SizedBox(width: 8), Text('Product A'), ], ), // ... more manual legend items Expanded(child: CristalyseChart()...), ], ) ``` ```dart After (Built-in) theme={null} // Automatic legend with one line CristalyseChart() .data(data) .mapping(x: 'month', y: 'sales', color: 'product') .geomBar() .legend() // ✨ Automatically creates legend for all products .build(); ``` ## Troubleshooting **Solution**: Ensure you have a `color` parameter in your `.mapping()` call: ```dart theme={null} .mapping(x: 'month', y: 'sales', color: 'category') // Required .legend() ``` **Solution**: Don't override text color - let it inherit from theme: ```dart theme={null} // Good - inherits theme color .legend(textStyle: TextStyle(fontSize: 14)) // Bad - hardcoded color .legend(textStyle: TextStyle(fontSize: 14, color: Colors.black)) ``` **Solution**: Choose a different position or add background: ```dart theme={null} .legend( position: LegendPosition.bottom, // Move to bottom backgroundColor: Colors.white.withValues(alpha: 0.9), ) ``` ## Y-Axis Titles in Legends **New in v1.13.0**: Display Y-axis titles alongside legend entries for multi-axis charts! For dual-axis charts, show axis titles in the legend to clarify which data series maps to which Y-axis: ```dart theme={null} CristalyseChart() .data(dualAxisData) .mapping(x: 'month', y: 'revenue') .mappingY2('conversion_rate') .geomBar(yAxis: YAxis.primary) .geomLine(yAxis: YAxis.secondary) .scaleYContinuous(title: 'Revenue ($K)') .scaleY2Continuous(title: 'Conversion Rate (%)') .legend( position: LegendPosition.bottom, showTitles: true, // Show axis titles in legend ) .build(); ``` **Features**: * Displays Y-axis titles alongside respective data series * Improves legend readability for multi-axis charts * Helps users understand which metric uses which axis * Works with all legend positioning options ### Parameters | Parameter | Type | Default | Description | | ------------ | ------ | ------- | --------------------------------------- | | `showTitles` | `bool` | `false` | Display Y-axis titles in legend entries | **Pro Tip**: Combine `showYAxisTitles` with descriptive axis titles for maximum clarity in multi-axis charts. ## What's Next? * Learn about [Custom Themes](/advanced/custom-themes) to create branded chart experiences * Explore [Interactions](/features/interactions) to add tooltips and hover effects * Check out [Export](/features/export) to save charts with legends as SVG files **Pro Tip**: Legends work seamlessly with all chart types and export formats. Your exported SVGs will include the positioned legends automatically! # Theming Source: https://docs.cristalyse.com/features/theming Customize visual appearance with comprehensive theming system ## Overview Cristalyse's theming system provides complete control over chart appearance. Choose from built-in themes or create custom designs that match your brand perfectly. ## Built-in Themes ### Default Light Theme Perfect for standard applications with clean, professional aesthetics: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'month', y: 'revenue') .geomBar() .theme(ChartTheme.defaultTheme()) .build() ``` ### Dark Theme Optimized for dark mode interfaces and low-light environments: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'category', y: 'value') .geomLine(strokeWidth: 3.0) .theme(ChartTheme.darkTheme()) .build() ``` ### Solarized Light Based on the popular Solarized color scheme, easy on the eyes: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'time', y: 'metric', color: 'series') .geomPoint(size: 6.0) .theme(ChartTheme.solarizedLightTheme()) .build() ``` ### Solarized Dark Dark variant of Solarized for comfortable night viewing: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'date', y: 'price') .geomArea(alpha: 0.4) .theme(ChartTheme.solarizedDarkTheme()) .build() ``` ## Theme Properties * `backgroundColor` - Chart container background * `plotBackgroundColor` - Data plotting area background * `primaryColor` - Default geometry color * `borderColor` - Chart borders and outlines * `gridColor` - Grid line color * `axisColor` - Axis lines and labels * `colorPalette` - Array of colors for multi-series data * `gridWidth` - Thickness of grid lines * `axisWidth` - Thickness of axis lines * `pointSizeDefault` - Default point size * `pointSizeMin` - Minimum point size for scaling * `pointSizeMax` - Maximum point size for scaling * `padding` - Chart margins and spacing * `axisTextStyle` - Style for axis tick labels * `axisLabelStyle` - Style for axis titles ## Creating Custom Themes ### Corporate Branding Create themes that match your brand colors: ```dart theme={null} final brandTheme = ChartTheme( backgroundColor: const Color(0xFFF8F9FA), plotBackgroundColor: Colors.white, primaryColor: const Color(0xFF007ACC), // Brand blue borderColor: const Color(0xFFE1E5E9), gridColor: const Color(0xFFF1F3F4), axisColor: const Color(0xFF5F6368), gridWidth: 0.5, axisWidth: 1.2, pointSizeDefault: 5.0, pointSizeMin: 3.0, pointSizeMax: 15.0, colorPalette: [ const Color(0xFF007ACC), // Primary blue const Color(0xFFFF6B35), // Orange accent const Color(0xFF28A745), // Success green const Color(0xFFDC3545), // Warning red const Color(0xFF6F42C1), // Purple const Color(0xFF20C997), // Teal ], padding: const EdgeInsets.fromLTRB(60, 20, 30, 50), axisTextStyle: const TextStyle( fontSize: 11, color: Color(0xFF5F6368), fontWeight: FontWeight.w500, ), axisLabelStyle: const TextStyle( fontSize: 13, color: Color(0xFF202124), fontWeight: FontWeight.w600, ), ); CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue', color: 'region') .geomBar(style: BarStyle.grouped) .theme(brandTheme) .build() ``` ### High Contrast Accessibility Optimize for users with visual impairments: ```dart theme={null} final accessibleTheme = ChartTheme( backgroundColor: Colors.white, plotBackgroundColor: Colors.white, primaryColor: Colors.black, borderColor: Colors.black, gridColor: const Color(0xFF666666), axisColor: Colors.black, gridWidth: 1.0, axisWidth: 2.0, pointSizeDefault: 8.0, pointSizeMin: 6.0, pointSizeMax: 20.0, colorPalette: [ Colors.black, const Color(0xFF0066CC), // High contrast blue const Color(0xFFCC0000), // High contrast red const Color(0xFF009900), // High contrast green const Color(0xFFFF6600), // High contrast orange ], padding: const EdgeInsets.all(20), axisTextStyle: const TextStyle( fontSize: 14, color: Colors.black, fontWeight: FontWeight.bold, ), ); ``` ### Minimal Theme Clean design for modern interfaces: ```dart theme={null} final minimalTheme = ChartTheme( backgroundColor: Colors.transparent, plotBackgroundColor: Colors.transparent, primaryColor: const Color(0xFF2196F3), borderColor: Colors.transparent, gridColor: const Color(0x1A000000), axisColor: const Color(0xFF757575), gridWidth: 0.5, axisWidth: 0.8, pointSizeDefault: 4.0, pointSizeMin: 2.0, pointSizeMax: 10.0, colorPalette: [ const Color(0xFF2196F3), const Color(0xFFFF5722), const Color(0xFF4CAF50), const Color(0xFFFFC107), const Color(0xFF9C27B0), ], padding: const EdgeInsets.fromLTRB(40, 10, 10, 30), axisTextStyle: const TextStyle( fontSize: 10, color: Color(0xFF757575), ), ); ``` ## Theme Modifications ### Using copyWith() Modify existing themes while preserving most properties: ```dart theme={null} // Customize default theme colors final customTheme = ChartTheme.defaultTheme().copyWith( primaryColor: Colors.deepPurple, colorPalette: [ Colors.deepPurple, Colors.amber, Colors.teal, Colors.pink, ], ); // Adjust dark theme spacing final spaciousTheme = ChartTheme.darkTheme().copyWith( padding: const EdgeInsets.all(40), pointSizeDefault: 8.0, ); // Update typography final boldTheme = ChartTheme.solarizedLightTheme().copyWith( axisTextStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Color(0xFF586e75), ), ); ``` ### Dynamic Theme Selection Switch themes based on app state: ```dart theme={null} class ThemeAwareChart extends StatelessWidget { final bool isDarkMode; final List> data; const ThemeAwareChart({ required this.isDarkMode, required this.data, super.key, }); @override Widget build(BuildContext context) { final theme = isDarkMode ? ChartTheme.darkTheme() : ChartTheme.defaultTheme(); return CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomLine() .theme(theme) .animate(duration: const Duration(milliseconds: 300)) .build(); } } ``` ## Custom Category Colors ### Using customPalette() Assign specific colors to chart categories instead of relying on theme defaults. Perfect for brand consistency and semantic coloring: ```dart theme={null} // Financial portfolio by asset class final assetColors = { 'Stocks': const Color(0xFF2563EB), // Blue - growth 'Bonds': const Color(0xFF059669), // Green - stability 'Real Estate': const Color(0xFFDC2626), // Red - tangible 'Commodities': const Color(0xFFF59E0B), // Yellow - resources 'Cash': const Color(0xFF6B7280), // Gray - liquid }; CristalyseChart() .data(portfolioData) .mapping(x: 'year', y: 'allocation', color: 'asset_class') .geomArea(strokeWidth: 1.0, alpha: 0.7) .theme(ChartTheme.defaultTheme()) .customPalette(categoryColors: assetColors) .build(); ``` ### Semantic Color Mapping Use meaningful colors for status, performance, or categorical data: ```dart theme={null} // Customer satisfaction survey results final satisfactionColors = { 'Very Satisfied': const Color(0xFF10B981), // Bright green 'Satisfied': const Color(0xFF84CC16), // Lime green 'Neutral': const Color(0xFFFBBF24), // Amber 'Dissatisfied': const Color(0xFFF97316), // Orange 'Very Dissatisfied': const Color(0xFFEF4444), // Red }; CristalyseChart() .data(surveyResults) .mapping(x: 'month', y: 'responses', color: 'satisfaction') .geomBar(style: BarStyle.stacked) .customPalette(categoryColors: satisfactionColors) .build(); ``` ### Department Analytics Consistent colors across organizational charts: ```dart theme={null} // Regional sales performance by continent final regionColors = { 'North America': const Color(0xFF3B82F6), // Blue - established market 'Europe': const Color(0xFF10B981), // Green - growth market 'Asia Pacific': const Color(0xFFF59E0B), // Yellow - emerging opportunity 'Latin America': const Color(0xFFEF4444), // Red - developing market 'Middle East': const Color(0xFF8B5CF6), // Purple - strategic market 'Africa': const Color(0xFF06B6D4), // Cyan - future potential }; CristalyseChart() .data(globalSalesData) .mapping(x: 'quarter', y: 'revenue', color: 'region') .geomLine(strokeWidth: 3.0) .geomPoint(size: 5.0) .customPalette(categoryColors: regionColors) .build(); ``` ### Smart Fallback Behavior Categories not specified in your custom palette automatically use theme defaults: ```dart theme={null} // Only specify colors for key categories final partialColors = { 'Primary': Colors.blue, 'Secondary': Colors.orange, // 'Other' categories will use theme.colorPalette colors }; CristalyseChart() .data(mixedData) .mapping(x: 'date', y: 'value', color: 'category') .geomArea() .customPalette(categoryColors: partialColors) .build(); ``` **Important**: The `customPalette()` method requires a `color` mapping in your `.mapping()` call. Categories not specified in `categoryColors` will automatically use colors from your theme's `colorPalette`. ## Advanced Theming Patterns ### Conditional Color Palettes Adapt colors based on data characteristics: ```dart theme={null} ChartTheme adaptiveTheme(List> data) { final categories = data.map((d) => d['category']).toSet().length; // Use different palettes based on data complexity final palette = categories <= 3 ? [Colors.blue, Colors.orange, Colors.green] : [ Colors.blue[400]!, Colors.blue[600]!, Colors.blue[800]!, Colors.orange[400]!, Colors.orange[600]!, Colors.orange[800]!, Colors.green[400]!, Colors.green[600]!, Colors.green[800]!, ]; return ChartTheme.defaultTheme().copyWith( colorPalette: palette, ); } ``` ### Responsive Theme Sizing Adjust theme properties based on screen size: ```dart theme={null} ChartTheme responsiveTheme(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; final isSmallScreen = screenWidth < 600; return ChartTheme.defaultTheme().copyWith( padding: EdgeInsets.all(isSmallScreen ? 16 : 32), axisTextStyle: TextStyle( fontSize: isSmallScreen ? 10 : 12, color: Colors.black87, ), pointSizeDefault: isSmallScreen ? 3.0 : 5.0, gridWidth: isSmallScreen ? 0.3 : 0.5, ); } ``` ## Best Practices * Ensure sufficient contrast ratios (4.5:1 minimum) * Test with colorblind-friendly palettes * Provide alternative visual cues beyond color * Consider WCAG 2.1 guidelines for data visualization * Use the same theme across related charts * Maintain consistent color meanings (red = danger, green = success) * Keep typography hierarchy consistent * Align with your app's overall design system * Reuse theme objects rather than creating new ones * Cache theme calculations for dynamic themes * Use `copyWith()` for minor modifications * Consider theme switching animation performance * Start with built-in themes as foundation * Test themes with various data types and sizes * Consider responsive design for different screen sizes * Document custom theme usage for team members * Use `customPalette()` for brand-specific or semantic colors * Maintain consistent color meanings across charts * Leverage fallback behavior for unmapped categories * Combine with themes for complete visual control * Test color accessibility and contrast ratios ## Theme Gallery Professional theme with brand-focused blue palette and clean typography WCAG-compliant theme optimized for visual accessibility Clean dark theme perfect for modern dashboards High-precision theme designed for scientific data visualization ## Next Steps Add hover effects and user interactions to themed charts Animate theme transitions and chart rendering Export themed charts with consistent styling Advanced techniques for creating complex theme systems # Cristalyse Docs Source: https://docs.cristalyse.com/index The grammar of graphics visualization library that Flutter developers have been waiting for Cristalyse Hero Light Cristalyse Hero Dark ## Welcome to Cristalyse **Finally, create beautiful data visualizations in Flutter without fighting against chart widgets or settling for web-based solutions.** Cristalyse brings the power of grammar of graphics (think ggplot2) to Flutter with buttery-smooth 60fps animations and true cross-platform deployment. Get your first chart running in 30 seconds Explore scatter plots, line charts, bars, and more Add tooltips, pan/zoom, and click handlers Real-world examples and complete code samples ## Why Choose Cristalyse? Familiar syntax if you've used ggplot2 or plotly. Build charts by layering data, mappings, and geometries instead of wrestling with rigid chart widgets. Leverages Flutter's rendering engine, not DOM manipulation. GPU-accelerated performance that handles large datasets without breaking a sweat. One codebase → Mobile, Web, Desktop, all looking identical. No platform-specific chart libraries or inconsistent rendering. Dual Y-axis support, stacked/grouped bars, interactive tooltips, SVG export, and custom theming for business-grade dashboards. ## Quick Example Here's how easy it is to create your first chart: ```dart theme={null} import 'package:cristalyse/cristalyse.dart'; import 'package:flutter/material.dart'; class MyChart extends StatelessWidget { @override Widget build(BuildContext context) { final data = [ {'x': 1, 'y': 2, 'category': 'A'}, {'x': 2, 'y': 3, 'category': 'B'}, {'x': 3, 'y': 1, 'category': 'A'}, {'x': 4, 'y': 4, 'category': 'C'}, ]; return CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint( size: 8.0, alpha: 0.8, shape: PointShape.triangle, borderWidth: 1.5, ) .scaleXContinuous() .scaleYContinuous() .theme(ChartTheme.defaultTheme()) .build(); } } ``` **Result:** A beautiful, animated scatter plot that works identically on iOS, Android, Web, and Desktop. ## What You Can Build Revenue vs conversion rate with dual Y-axis support Interactive scatter plots with hover tooltips Multi-series line charts with smooth animations Stacked bar charts for budget breakdowns Area charts showing engagement over time Touch-friendly charts with pan and zoom ## Perfect For * **Flutter developers** building data-driven apps who need more than basic chart widgets * **Data scientists** who want to deploy interactive visualizations to mobile without learning Swift/Kotlin * **Enterprise teams** building dashboards that need consistent UX across all platforms * **Business analysts** creating professional reports with dual Y-axis charts and advanced visualizations ## Getting Started Add Cristalyse to your Flutter project with a single command Create your first scatter plot in under 30 seconds Discover animations, theming, and interactive capabilities Create production-ready data visualizations Ready to get started? [Install Cristalyse](/installation) and create your first chart! # Installation Source: https://docs.cristalyse.com/installation Add Cristalyse to your Flutter project in seconds ## Quick Installation Adding Cristalyse to your Flutter project is incredibly simple. Just run one command: ```bash theme={null} flutter pub add cristalyse ``` That's it! No complex setup, no additional configuration needed. ## Manual Installation If you prefer to add the dependency manually, update your `pubspec.yaml`: ```yaml pubspec.yaml theme={null} dependencies: flutter: sdk: flutter cristalyse: ^1.17.6 # Latest version ``` Then run: ```bash theme={null} flutter pub get ``` ## Verify Installation Create a simple test to verify everything is working: ```dart lib/test_chart.dart theme={null} import 'package:cristalyse/cristalyse.dart'; import 'package:flutter/material.dart'; class TestChart extends StatelessWidget { @override Widget build(BuildContext context) { return CristalyseChart() .data([ {'x': 1, 'y': 2}, {'x': 2, 'y': 3}, {'x': 3, 'y': 1}, ]) .mapping(x: 'x', y: 'y') .geomPoint() .build(); } } ``` If this compiles without errors, you're ready to go! ## Platform Support Cristalyse works on all Flutter-supported platforms: iOS 12+\ Android API 21+ Windows 10+\ macOS 10.14+\ Linux (Ubuntu 18.04+) Chrome 80+\ Firefox\ Safari Flutter 1.17+\ Dart 3.0+\ Null Safety ## Requirements * **Flutter SDK**: 1.17.0 or higher * **Dart SDK**: 3.0.0 or higher * **Platforms**: iOS, Android, Web, Windows, macOS, Linux ## Dependencies Cristalyse has minimal dependencies to keep your app lightweight: * `flutter` (SDK) * `path_provider` (for SVG export functionality) * `flutter_svg` (for vector graphics support) ## Version History | Version | Release Date | Key Features | | ------- | -------------- | ------------------------------------------------------------------------------------------- | | 1.17.6 | April 2026 | Fixed tooltip flickering when moving the mouse over the same data point | | 1.17.5 | February 2026 | Dynamic chart padding optimization, memory leak patches, and geometry color priority fixes | | 1.17.4 | February 2026 | Example app UI overhaul with dark mode, 13 new palettes, and gesture fixes | | 1.17.3 | February 2026 | Fixed OrdinalScale inversion overflow and enhanced bar chart examples | | 1.17.2 | December 2025 | Fixed tooltip positioning when panning | | 1.17.1 | December 2025 | Fixed tooltips not working with interactive legends | | 1.17.0 | December 2025 | Bar chart positive/negative value styling with smart rounded corners and conditional colors | | 1.16.0 | November 2025 | Integer-only ticks for continuous scales | | 1.15.0 | November 2025 | Pinch, scroll wheel, and button-based zoom interactions | | 1.14.0 | November 2025 | Tick configuration for continuous scales | | 1.13.1 | November 2025 | Fixed label size calculation with default font size | | 1.13.0 | November 2025 | Optional Y-axis titles in legends | | 1.12.1 | November 2025 | Fixed right padding and secondary Y-axis alignment | | 1.12.0 | November 2025 | Boundary clamping for pan operations | | 1.11.1 | October 2025 | Fixed Y-axis bounds during X-axis panning | | 1.11.0 | October 2025 | Programmatic pan controller with panTo() and panReset() methods | | 1.10.3 | October 2025 | Scale padding fix for consistent chart sizing during pan operations | | 1.10.2 | October 2025 | Heat map color unification and bounds edge case validation | | 1.10.1 | October 2025 | Wilkinson labeling precision fix for large numbers | | 1.10.0 | October 2025 | Axis titles and bubble size guide with edge case validation | | 1.9.0 | October 2025 | Interactive & floating legends with click-to-toggle and custom positioning | | 1.8.1 | October 2025 | Bidirectional documentation links and enhanced developer experience | | 1.8.0 | October 2025 | Intelligent axis bounds with Wilkinson labeling algorithm | | 1.7.0 | September 2025 | Progress bar charts with advanced styles and documentation improvements | | 1.6.1 | September 2025 | MCP Server integration for AI coding assistants | | 1.6.0 | September 2025 | Gradient color support for enhanced visualizations | | 1.5.0 | September 2025 | Built-in legend support with dark mode compatibility | | 1.4.0 | September 2025 | Custom category colors with palette system | | 1.3.1 | September 2025 | Multi-series line chart rendering fixes | | 1.3.0 | September 2025 | Comprehensive bubble chart support | | 1.2.4 | August 2025 | Performance improvements and bug fixes | | 1.2.3 | August 2025 | Documentation reference updates | | 1.2.2 | August 2025 | Enhanced HeatMap text readability | | 1.2.1 | August 2025 | Dependencies update and pie chart screenshots | | 1.2.0 | August 2025 | Heat map chart support with animations | | 1.1.0 | August 2025 | Advanced label formatting system | | 1.0.1 | July 2025 | Grouped bar chart alignment fixes | | 1.0.0 | July 2025 | Comprehensive pie chart and donut chart support | | 0.9.4 | July 2025 | Web WASM compatibility improvements | | 0.9.3 | July 2025 | Documentation site launch | | 0.9.2 | July 2025 | Advanced pan control with visual clipping | | 0.9.0 | July 2025 | Enhanced SVG export implementation | | 0.8.0 | July 2025 | Area chart support | | 0.7.0 | June 2025 | Interactive panning system | | 0.6.0 | June 2025 | Interactive tooltips and click handlers | | 0.5.0 | June 2025 | Dual Y-axis support | ## Next Steps Now that you have Cristalyse installed, let's create your first chart: Build your first chart in 30 seconds ## Troubleshooting Make sure you're running Flutter 1.17+ and Dart 3.0+: ```bash theme={null} flutter --version dart --version ``` Verify the import statement: ```dart theme={null} import 'package:cristalyse/cristalyse.dart'; ``` Make sure you're using a supported browser (Chrome 80+, Firefox, Safari). Cristalyse is optimized for modern devices. For older Android devices (API \< 21), consider reducing animation complexity. Need help? Check our [GitHub Issues](https://github.com/rudi-q/cristalyse/issues) or [start a discussion](https://github.com/rudi-q/cristalyse/discussions). # Quick Start Source: https://docs.cristalyse.com/quickstart Create your first chart in 30 seconds ## Your First Chart in 30 Seconds Let's create a beautiful scatter plot with just a few lines of code. Add the import to your Dart file: ```dart theme={null} import 'package:cristalyse/cristalyse.dart'; import 'package:flutter/material.dart'; ``` Create a simple dataset: ```dart theme={null} final data = [ {'x': 1, 'y': 2, 'category': 'A'}, {'x': 2, 'y': 3, 'category': 'B'}, {'x': 3, 'y': 1, 'category': 'A'}, {'x': 4, 'y': 4, 'category': 'C'}, ]; ``` Create the chart with grammar of graphics: ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint( size: 8.0, alpha: 0.8, shape: PointShape.triangle, borderWidth: 1.5, ) .scaleXContinuous() .scaleYContinuous() .theme(ChartTheme.defaultTheme()) .build() ``` ## Complete Example Here's the full widget ready to use: ```dart theme={null} import 'package:cristalyse/cristalyse.dart'; import 'package:flutter/material.dart'; class MyFirstChart extends StatelessWidget { @override Widget build(BuildContext context) { final data = [ {'x': 1, 'y': 2, 'category': 'A'}, {'x': 2, 'y': 3, 'category': 'B'}, {'x': 3, 'y': 1, 'category': 'A'}, {'x': 4, 'y': 4, 'category': 'C'}, ]; return Scaffold( appBar: AppBar(title: Text('My First Chart')), body: Padding( padding: EdgeInsets.all(16.0), child: CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint( size: 8.0, alpha: 0.8, shape: PointShape.triangle, borderWidth: 1.5, ) .scaleXContinuous() .scaleYContinuous() .theme(ChartTheme.defaultTheme()) .legend() // ✨ Add a legend to show categories .build(), ), ); } } ``` **Result:** A beautiful, animated scatter plot with color-coded categories!
Your First Chart Result
Your first chart - clean, responsive, and cross-platform
## Formatting Axis Labels You'll often want formatted axis labels. The `labels` parameter takes any function that converts a number to a string: ```dart theme={null} CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue') .geomBar() .scaleYContinuous(labels: (value) => '\$${value}') // $150 .scaleXOrdinal() .build() ``` For robust, locale-aware formatting, use NumberFormat: ```dart theme={null} import 'package:intl/intl.dart'; CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue') .geomBar() .scaleYContinuous(labels: NumberFormat.simpleCurrency().format) // $1,234.56 .scaleXOrdinal() .build() ``` ## Adding a Legend **New in v1.5.0**: Built-in legend support with zero configuration! When your chart has color categories, add a professional legend with one line: ```dart theme={null} final salesData = [ {'quarter': 'Q1', 'revenue': 1000, 'product': 'ProductA'}, {'quarter': 'Q2', 'revenue': 1200, 'product': 'ProductA'}, {'quarter': 'Q1', 'revenue': 800, 'product': 'ProductB'}, {'quarter': 'Q2', 'revenue': 950, 'product': 'ProductB'}, ]; CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue', color: 'product') // Color mapping required .geomBar(style: BarStyle.grouped) .legend() // ✨ Automatically creates legend for each product .build(); ``` **Key Benefits:** * **Zero Config**: Just add `.legend()` and it works * **Smart Positioning**: Defaults to top-right, 8 position options available * **Dark Mode Ready**: Text automatically adapts to theme colors * **Auto-Symbols**: Squares for bars, circles for points, lines for line charts **Requirement**: Legends need a `color` parameter in your `.mapping()` call to detect categories. ### Quick Legend Positioning ```dart theme={null} // Bottom legend (great for mobile) .legend(position: LegendPosition.bottom) // Right side legend .legend(position: LegendPosition.right) // Top-left corner .legend(position: LegendPosition.topLeft) ``` Learn more about [legend customization and styling](/features/legends). ## Understanding the Grammar Cristalyse follows the **Grammar of Graphics** pattern: `.data()` - Your dataset as a List of Map objects `.mapping()` - Connect data columns to visual properties (x, y, color, size) `.geomPoint()`, `.geomLine()`, `.geomBar()` - How to visualize the data `.scaleXContinuous()`, `.scaleYContinuous()` - Transform data to screen coordinates `.theme()` - Colors, fonts, and visual styling # Updates Source: https://docs.cristalyse.com/updates Latest features, improvements, and fixes in Cristalyse # Updates & Changelog Stay informed about the latest features, improvements, and fixes in Cristalyse. ### 🐛 Tooltip Flickering Fix **Smooth tooltips, no more jitter!** **What was fixed:** * **Eliminated tooltip flicker:** Tooltips no longer close and reopen when the mouse moves slightly while hovering over the same data point. * **Smart Point Detection:** The chart now intelligently detects when the hovered data point hasn't changed. If the tooltip content would be identical, the existing tooltip is preserved instead of being recreated. * **Comparison Logic:** Points are identified by `dataIndex + seriesName`, ensuring the fix works seamlessly with both single-series and multi-series charts. **Why this matters:** * Users can now hover naturally over data points without experiencing visual jitter. * Tooltips feel stable and responsive across all chart types. * No performance impact — the optimization actually reduces unnecessary DOM updates. **Quality Assurance:** * 297 tests pass — zero breaking changes. * Fully backward compatible. * Safe upgrade for all existing projects. ### 🐛 Layout Optimizations & Bug Fixes **Crisper layouts, less wasted space, and perfect combo charts!** **What was fixed:** * **Dynamic Left Padding:** Removed the hardcoded 80px left padding fallback. The chart now dynamically calculates the exact width needed for Y-axis labels and heatmaps. * **Memory & Clipping:** Fixed heatmap Y-axis layout clipping and resolved a `TextPainter` memory leak. * **Geometry Color Priorities:** Fixed a bug where explicit `geomLine(color: ...)` overrides were ignored if the data had categorical color mappings. You can now cleanly mix and match color logic in combo charts! * **Theme Adjustments:** Default `ChartTheme` padding has been tightened for a much cleaner out-of-the-box appearance. **Quality Assurance:** * 100% backward compatible API. * Fixes apply automatically to existing setups. ### 🎨 Example App UI Overhaul & Fixes **Major visual upgrade for the example app!** * **Dark Mode Support**: Example app now automatically adapts to system brightness. * **Theme & Palette Expansion**: Added **13 new color palettes** (Ocean, Warm, Neon, etc.) and High Contrast / Solarized themes. * **Gesture Fixes**: Replaced `SelectableText` with `Text` across interactive widgets to resolve touch conflicts. * **Code Quality**: Refactored theme management for better type safety and maintainability. **Quality Assurance:** * Zero changes to library code (safe upgrade). **Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this contribution! **Reviewed and documented by maintainer [@rudi-q](https://github.com/rudi-q)** ### 🐛 OrdinalScale Inversion Fix **Fixed coordinate inversion at scale boundaries!** **What was fixed:** * `OrdinalScale.invert()` now correctly maps screen coordinates back to domain categories even at the very edges of the range. * Previously, a calculation mismatch could cause index overflows or incorrect category selection. * The fix ensures mathematical consistency between forward scaling and inverse mapping. ### 📊 Enhanced Bar Chart Examples **Better multi-series demonstration!** * Added grouped bar series to the example app data. * Demonstrated color mapping for categorical data in standard bar charts. * Improved data visualization examples for developers. **Quality Assurance:** * Zero breaking changes. * Fully backward compatible. ### 🐛 Tooltip Offset Fix **Fixed tooltip positioning when panning!** **What was fixed:** * Tooltips now correctly align with data points even after panning the chart. * Previously, a mismatch between the estimated and actual plot area caused offsets. * The fix ensures the interaction detector uses the exact geometry calculated by the painter. **Quality Assurance:** * Zero breaking changes. * Fully backward compatible. ### 🐛 Interactive Legend Tooltip Fix **Tooltips now work correctly with interactive legends!** **What was fixed:** * Tooltips now appear when hovering over charts with `interactive: true` legends * Previously, enabling interactive legends caused tooltips to stop working * Affected chart types: line charts, area charts, bar charts, and all other geometries **Root Cause:** * When interactive legends filter data, the chart creates a new widget for filtered data * This filtered chart was missing the `ChartTooltipOverlay` wrapper * Fix ensures tooltip overlay is properly applied to filtered charts **Example:** ```dart theme={null} // Now works perfectly together! CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar(style: BarStyle.grouped) .legend(interactive: true) // ✅ Click to toggle categories .interaction( tooltip: TooltipConfig( builder: (point) => Text( '${point.getDisplayValue('product')}: \$${point.getDisplayValue('revenue')}k', ), ), ) .build() ``` **Quality Assurance:** * All 297 tests passing * Zero breaking changes * Example app updated with demonstration [View legend documentation](/features/legends) ### 📊 Bar Chart Positive/Negative Value Styling **Smart rounded corners and conditional colors for financial charts!** **Smart Rounded Corners (`roundOutwardEdges`):** * Positive bars: Rounded top (vertical) or right (horizontal) * Negative bars: Rounded bottom (vertical) or left (horizontal) * Sharp edge at zero baseline for clean alignment * Works seamlessly with existing `borderRadius` parameter **Conditional Bar Colors:** * New `positiveColor` parameter for values >= 0 * New `negativeColor` parameter for values \< 0 * Smart fallback: positiveColor/negativeColor → color → colorScale → theme * Perfect for profit/loss, variance analysis, financial dashboards **Use Cases:** * Financial dashboards showing gains and losses * Variance charts highlighting positive and negative deviations * Performance metrics with above/below target visualization * Budget analysis with overspending and savings ```dart theme={null} // Financial Dashboard Example final profitLossData = [ {'month': 'Jan', 'pnl': 45000.0}, {'month': 'Feb', 'pnl': -12000.0}, {'month': 'Mar', 'pnl': 67000.0}, {'month': 'Apr', 'pnl': -8000.0}, ]; CristalyseChart() .data(profitLossData) .mapping(x: 'month', y: 'pnl') .geomBar( borderRadius: BorderRadius.circular(12), roundOutwardEdges: true, // Smart rounding positiveColor: Colors.green, // Gains negativeColor: Colors.red, // Losses ) .scaleXOrdinal() .scaleYContinuous() .build() ``` **Enhanced Example App:** * Updated bar chart demo with positive/negative value showcase * Live demonstration at `#/bar-chart` route * Visual comparison of standard vs. smart-rounded bars **Bug Fixes:** * Fixed negative bar rendering (bars extending below zero) * Corrected bar rect calculation for negative heights **Code Quality:** * Extracted `_computeOutwardRRect()` helper to eliminate code duplication * Optimized example with const widgets and top-level data **Quality Assurance:** * Zero breaking changes - fully backward compatible * All new parameters optional (default to `false`/`null`) * `flutter analyze` passes with no issues [View bar chart documentation](/charts/bar-charts#positivenegative-value-styling) ### 📏 Integer-Only Ticks **Force axis ticks to be integers!** **Integer-Only Ticks:** * New `integersOnly` parameter in `TickConfig` * Automatically clamps ticks to integer values * Ensures step size is at least 1 * Ideal for count data (people, items, events) where fractional values don't make sense **Use Cases:** * Count data (e.g., number of users, items sold) * Discrete events * Avoiding "1.5 people" in charts ```dart theme={null} CristalyseChart() .scaleYContinuous( tickConfig: TickConfig( integersOnly: true, ), ) ``` **Quality Assurance:** * Zero breaking changes - fully backward compatible * Production ready ### 🔍 Zoom & Pan Interactions **Pinch, scroll wheel, and button-based zooming!** **Zoom Interaction Modes:** * Pinch gestures for multi-touch zooming * Scroll wheel support for precise zoom control * Floating +/- buttons for touch-friendly interfaces * Independent X-axis, Y-axis, or dual-axis zooming **Zoom Configuration:** * New `ZoomConfig` class with extensive customization * `axes` parameter: Choose `ZoomAxis.x`, `ZoomAxis.y`, or `ZoomAxis.both` * `wheelSensitivity`: Fine-tune scroll wheel responsiveness (0.0005-0.0035) * `buttonStep`: Control zoom increment for +/- buttons (default 1.4x) * `maxScale` / `minScale`: Set zoom boundaries (default 16x / 1x) * `showButtons`: Toggle floating control buttons * `buttonAlignment` & `buttonPadding`: Customize button placement **Zoom Event Callbacks:** * `onZoomStart`, `onZoomUpdate`, `onZoomEnd` for lifecycle events * `ZoomInfo` provides real-time zoom metrics: * Current visible range: `visibleMinX`, `visibleMaxX`, `visibleMinY`, `visibleMaxY` * Scale factors: `scaleX`, `scaleY` (e.g., 4.0 = 4x zoom) * State: `ZoomState.start` / `update` / `end` **Quick Setup:** ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'day', y: 'revenue') .geomLine() .onZoom((info) { print('Zoom scale: ${info.scaleX}x'); }) .build() ``` **Advanced Configuration:** ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'time', y: 'value') .geomLine() .interaction( zoom: ZoomConfig( enabled: true, axes: ZoomAxis.both, // Zoom both axes maxScale: 16.0, // Up to 16x zoom wheelSensitivity: 0.0015, // Scroll sensitivity buttonStep: 1.4, // 40% step per button press showButtons: true, // Show +/- buttons onZoomUpdate: (info) { setState(() { zoomLevel = '${info.scaleX?.toStringAsFixed(1)}x'; }); }, ), ) .build() ``` **New Example:** * Full zoom demo in example app showing all interaction methods * Live zoom state display with visible ranges and scale factors * Interactive controls to adjust sensitivity and zoom modes * Demonstrates combined zoom + pan workflows **Use Cases:** * Time-series exploration: Zoom into specific date ranges * Scientific visualizations: Independent X/Y axis zoom control * Mobile dashboards: Touch-friendly zoom buttons for accessibility * Financial charts: Precise zoom with scroll wheel support **Seamless Integration:** * Works alongside existing pan interactions * Respects domain boundaries and scale configuration * Responsive gesture handling (pinch, scroll, single-touch pan) * Full touch and mouse support across all platforms **Quality Assurance:** * Zero breaking changes - fully backward compatible * All new parameters optional with sensible defaults [View zoom documentation](/features/interactions) ### 🎯 Tick Configuration for Scales **Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this contribution! **Reviewed and documented by maintainer [@rudi-q](https://github.com/rudi-q)** **Control Tick Generation:** * New `TickConfig` class for customizing tick marks and labels * `ticks` parameter: Specify explicit tick positions for precise control * `simpleLinear` flag: Generate uniform linear spacing instead of Wilkinson's algorithm **Use Cases:** * Industry-standard reference points (freezing point, percentiles, etc.) * Time-series charts with consistent intervals * Scientific/technical charts requiring uniform spacing * Matching ticks across multiple related charts **Example:** ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'time', y: 'value') .scaleXContinuous( tickConfig: TickConfig(simpleLinear: true), // Uniform spacing ) .scaleYContinuous( tickConfig: TickConfig(ticks: [0, 25, 50, 75, 100]), // Custom ticks ) .geomLine() .build() ``` **New Example:** * TimeBasedLineChartWidget demonstrating TickConfig usage * Toggle between simple linear and Wilkinson's algorithm * Practical reference for common use cases **Technical Implementation:** * Added `TickConfig` class with `ticks` and `simpleLinear` properties * Extended `LinearScale` to accept and store `TickConfig` * Enhanced Wilkinson labeling algorithm with simple linear fallback * All three scale methods (`scaleXContinuous`, `scaleYContinuous`, `scaleY2Continuous`) now support `tickConfig` **Quality Assurance:** * Zero breaking changes - fully backward compatible * New feature is opt-in with sensible defaults * Production ready [View tick configuration documentation](/advanced/scales) ### 🐛 Label Size Calculation Fix **Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this fix! **Fixed label size deduction based on formatter** * Label size now calculated based on actual text width with default font size * Ensures consistent and accurate label spacing in charts * Improves readability with properly-sized axis labels **Technical Improvements:** * Added `_calculatePixelsPerLabel()` method to measure text dimensions * Uses TextPainter with default TextStyle (fontSize: 12) for accurate measurement * Includes proper padding calculation (10px) * Applied to both LinearScale and OrdinalScale * Made `optimalPixelsPerLabel` private **Benefits:** * More accurate label sizing across all chart types * Better space utilization for axis labels * Consistent behavior regardless of label content **Quality Assurance:** * Zero breaking changes - fully backward compatible * Focused single-file bug fix * Production ready ### 🎨 Legend Enhancements **Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this contribution! **Optional Y-Axis Titles in Legends!** **Legend Enhancements:** * New `showTitles` option to display Y-axis titles alongside legend entries * Improves chart legend readability when multiple Y-axes are present * Optional feature - legends work as before by default * Seamless integration with existing legend styling and layout **Use Cases:** * Multi-axis charts with clearer data context * Better legend readability for complex visualizations * Maintain visual hierarchy with optional titles **Technical Implementation:** * Extended `LegendWidget` to support optional Y-axis title rendering * Enhanced `legend.dart` core logic to handle title display * Updated chart configuration to expose new option * Comprehensive test coverage included **Quality Assurance:** * All existing tests continue to pass * Zero breaking changes - fully backward compatible * New feature is opt-in * Production ready ### 🐛 Right Padding & Secondary Y-Axis Fixes **Fixed excessive right padding and layout misalignment** * Removed hardcoded 80px padding applied unconditionally for secondary y-axis * Charts without secondary y-axis now use only theme padding (no waste) * Widget layout uses conservative estimate only when y2Scale exists * Painter performs precise calculation at paint time with full scale information * Eliminates divergence between layout and rendering phases * Added proper null checking for `y2Scale` to prevent unnecessary padding **Benefits:** * Better space utilization on single-axis charts * Proper secondary y-axis spacing based on actual label widths * Ensures hit-testing alignment for interactions (hover, click, pan) * Consistent plot area between layout and render phases **Quality Assurance:** * Zero breaking changes - fully backward compatible * Improved interaction reliability with secondary y-axis charts * Maintains consistent dimensions during pan/zoom operations ### 🏔️ Boundary Clamping for Pan Operations **Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this contribution! **Control infinite panning with boundary clamping!** **Boundary Clamping:** * New `boundaryClampingX` and `boundaryClampingY` options in `PanConfig` * Prevents panning beyond data boundaries when enabled * Maintains intuitive pan behavior within configured domain limits * Perfect for constrained data exploration and guided navigation **Use Cases:** * Prevent users from panning too far from relevant data * Create bounded exploration areas for large datasets * Maintain data context during navigation * Improve UX for time-series and scientific visualizations ```dart theme={null} CristalyseChart() .data(timeSeriesData) .mapping(x: 'time', y: 'value') .geomLine() .interaction( pan: PanConfig( enabled: true, boundaryClampingX: true, // Clamp X-axis panning boundaryClampingY: true, // Clamp Y-axis panning ), ) .build() ``` **Technical Implementation:** * Scale boundaries tracked via `valuesBoundaries` in `LinearScale` * Pan domain clamping applied during interaction handling * Seamless integration with existing pan callbacks and pan controller * No changes to default behavior - opt-in feature **Quality Assurance:** * Zero breaking changes - fully backward compatible * Default clamping disabled (infinite panning by default) * Tested with pan controller and manual pan interactions * Production ready ### 🐛 Y-axis Bounds Fix During Panning **Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this fix! **Fixed Y-axis getting stuck during X-axis pan operations** * Y-axis bounds now correctly update when configured with `updateYDomain: false` * Previously remained locked to original `panYDomain` bounds during X-axis panning * Enables proper dynamic Y-axis scaling while panning horizontally **Quality Assurance:** * Zero breaking changes - fully backward compatible * Patch bump release ### 🎯 Programmatic Pan Controller **Code authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this contribution! **New PanController Class:** * External control of chart panning via new `PanController` class * `panTo(PanInfo)` method for programmatic pan operations * `panReset()` method to restore original chart view * ChangeNotifier-based architecture for reactive updates * Optional `controller` parameter in `PanConfig` **Enhanced Pan Configuration:** * Widget lifecycle management (initState, didUpdateWidget, dispose) * Automatic listener registration and cleanup * Full integration with existing pan callbacks ```dart theme={null} final panController = PanController(); CristalyseChart() .data(data) .mapping(x: 'x', y: 'y') .geomLine() .interaction( pan: PanConfig( enabled: true, controller: panController, ), ) .build(); // Pan to specific range panController.panTo(PanInfo( visibleMinX: 500, visibleMaxX: 1500, state: PanState.update, )); // Reset to original view panController.panReset(); ``` **Use Cases:** * Programmatic zoom controls with buttons/sliders * Reset to original view functionality * Coordinated panning across multiple charts * External UI controls for chart navigation * Jump to specific data ranges programmatically **Quality Assurance:** * Zero breaking changes - fully backward compatible * Optional controller parameter (defaults to null) * Proper lifecycle management with listener cleanup * Example integration in pan\_example.dart ### 🐛 Scale Padding Fix **Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this fix! **Fixed chart shrinking when panning** * Scale padding now initialized before painting charts * Prevents setupYScale from changing padding during panning * Chart maintains consistent size during all pan operations * Smooth user experience without unintended resize behavior **Quality Assurance:** * All existing tests continue to pass * Zero breaking changes - fully backward compatible ### 🎨 Heat Map Unification & Bounds Validation **Authored by [@davidlrichmond](https://github.com/davidlrichmond)** - Thank you for this contribution! **Heat Map Color System Unification:** * Unified heat maps to use `GradientColorScale` for consistent color handling * Eliminates duplicate color logic between heat maps and other chart types * Improves code maintainability and reduces technical debt * Heat map colors now follow the same scaling principles as other geometries **Bounds Edge Cases & Validation:** * Fixed guard condition for corner case where `min > max` in bounds calculations * Prevents invalid scale configurations that could crash rendering * Added comprehensive test coverage for edge cases * Improved documentation for bounds behavior with notes on invalid configurations **Code Quality:** * Addressed code review feedback on refactored code * Fixed deprecated `int` RGB getters for Flutter compatibility * Applied code formatting linter fixes **Quality Assurance:** * Added 34 new test cases for bounds edge cases * Added 106 lines of tests for GradientColorScale functionality * All existing tests continue to pass * Zero breaking changes - fully backward compatible ### 🐛 Wilkinson Labeling Precision **Authored by [@jbbjarnason](https://github.com/jbbjarnason)** - Thank you for this fix! **Fixed floating-point rounding in epoch millisecond label calculations** * Replaced `round()` with `roundToDouble()` for proper double precision handling * Resolves issues with large number labeling (e.g., epoch timestamps) * Added comprehensive test case for epoch millisecond labeling * Test validates correct tick generation: \[1760527000000.0, 1760528000000.0, 1760529000000.0, 1760530000000.0] **Technical Details:** * `_cleanNumber()` method in `WilkinsonLabeling` class now uses `roundToDouble()` instead of `round()` * Fixes edge case with very large timestamp values (>1.7 trillion milliseconds) * Maintains precision in float arithmetic for astronomical numbers **Quality Assurance:** * All 286 tests passing (285 existing + 1 new test) * Zero breaking changes - fully backward compatible ### 🎨 Axis Titles & Bubble Size Guide **Descriptive axis titles and visual bubble size legends!** **Axis Titles:** * Add titles to X, Y, and Y2 axes with optional `title` parameter * Smart positioning with automatic spacing calculations * Vertical axes display rotated titles (-90° for Y, +90° for Y2) * Theme-aware styling with customizable fonts **Bubble Size Guide:** * Visual legend shows min, mid, and max bubble sizes * Automatically appears when `title` provided on `geomBubble()` * Horizontal and vertical layout support * Seamlessly integrates with existing legend system ```dart theme={null} // Axis titles for clearer data context CristalyseChart() .data(timeSeriesData) .mapping(x: 'time', y: 'value') .geomLine() .scaleXContinuous(title: 'Time (seconds)') .scaleYContinuous(title: 'Temperature (°C)') .build() // Dual axis with titles CristalyseChart() .data(businessData) .mapping(x: 'month', y: 'revenue') .mappingY2('conversion') .geomBar(yAxis: YAxis.primary) .geomLine(yAxis: YAxis.secondary) .scaleXOrdinal(title: 'Month') .scaleYContinuous(title: 'Revenue ($K)') .scaleY2Continuous(title: 'Conversion Rate (%)') .build() // Bubble size guide in legend CristalyseChart() .data(companyData) .mapping(x: 'revenue', y: 'customers', size: 'marketShare') .geomBubble( title: 'Market Share (%)', // Enables size guide minSize: 8.0, maxSize: 25.0, ) .legend() // Shows both category colors and size guide .build() ``` **Bug Fixes:** * Fixed edge case where zero/negative bubble sizes could cause rendering errors * Added validation to ensure bubble legend sizes are always positive * Clamps invalid values to safe minimum (1.0px radius) * Prevents Container dimension errors with edge-case data **Technical Improvements:** * Precise spacing calculations for axis labels and titles * Pre-calculated label dimensions for optimal layout * Consistent spacing constants across all axes * Validated bubble sizes in legend rendering * 8 new edge case tests for bubble validation **Quality Assurance:** * All 285 tests passing (20 new tests added) * Zero breaking changes - fully backward compatible * Titles optional and render only when provided * Production ready ### 🎯 Interactive & Floating Legends **Authored by [@davidlrichmond](https://github.com/davidlrichmond)** - Valuable contribution! **Transform your charts with interactive legend controls and advanced positioning!** **Interactive Legend Toggle:** * Click-to-hide/show data points with visual feedback * Simple `.legend(interactive: true)` to enable * Toggled items show reduced opacity + strikethrough styling * Smooth chart updates with preserved color consistency * Both auto-managed and external state control **Floating Legend Positioning:** * New `LegendPosition.floating` with custom offsets * Negative offsets supported (legends outside chart bounds!) * `clipBehavior: Clip.none` enables overflow rendering * Full creative control over placement ```dart theme={null} // Interactive Legend (Auto-Managed) CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar(style: BarStyle.grouped) .legend(interactive: true) // Click to toggle! .build() // Floating Legend with Custom Position CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomPoint() .legend( position: LegendPosition.floating, floatingOffset: Offset(20, 20), // Custom position ) .build() // Interactive + Floating + External State CristalyseChart() .data(data) .mapping(x: 'x', y: 'y', color: 'category') .geomLine() .legend( interactive: true, position: LegendPosition.floating, floatingOffset: Offset(-10, 30), // Can overflow! hiddenCategories: myHiddenSet, onToggle: (category, visible) => handleToggle(category, visible), ) .build() ``` **Enhanced API:** * `interactive`: Enable click-to-toggle functionality * `hiddenCategories`: External state management * `onToggle`: Custom toggle callbacks * `floatingOffset`: Precise Offset positioning **Color Consistency Fixed:** * Original ColorScale preserved when filtering * No color shifting when toggling categories * Chart painter respects provided ColorScale **Theme-Aware Backgrounds:** * Legend backgrounds adapt to theme colors * Automatic contrast for light/dark modes * No hardcoded white backgrounds **Quality Assurance:** * All 268 tests passing * Zero breaking changes * Production ready [View legend documentation](/features/legends) ### 🔗 Enhanced Developer Experience **Seamless navigation between documentation and live examples!** * **Bidirectional Links**: Jump between docs and example app instantly * **View Docs Button**: All 19 example app screens link to documentation * **View Live Example Cards**: 9 chart docs pages link to interactive examples * **Better Learning Flow**: Quickly switch from reading to seeing live code **Documentation Navigation:** * Scatter plots, line charts, bar charts, area charts * Bubble charts, pie charts, dual-axis charts * Heat maps, progress bars * Opens in appropriate context (external browser for app, new tab for docs) **Technical Improvements:** * Clean architecture with single source of truth for URL mappings * Type-safe implementation with proper null checks * Consistent UI patterns across both navigation methods * Added `url_launcher` package for external link handling ```dart theme={null} // Example app now has "View Docs" button on each chart // Documentation now has "View Live Example" cards // Perfect for learning and exploring! ``` **Quality Assurance:** * All Flutter analyze checks passing * Zero breaking changes * Backward compatible ### 🎯 Intelligent Axis Bounds & Labeling **Authored by [@davidlrichmond](https://github.com/davidlrichmond)** - Exceptional contribution! **Wilkinson Extended Algorithm for professional axis labels:** * Based on IEEE paper by Talbot, Lin, and Hanrahan (2010) * Generates "nice" round numbers: 0, 5, 10, 50, 100 * Optimizes simplicity, coverage, density, and legibility * Smart pruning for performance **Geometry-Aware Bounds:** * Bar/area charts: Zero baseline for quantity comparison * Line/scatter charts: Data-driven bounds for trend analysis * Automatic appropriate defaults based on chart type ```dart theme={null} // Prettier axis labels automatically! // Before: 0.47, 5.23, 10.88, 15.91, 21.07 // After: 0, 5, 10, 15, 20 CristalyseChart() .data(data) .mapping(x: 'time', y: 'value') .geomLine() .scaleYContinuous() // Smart bounds automatically! .build() // New bubble size limits chart.geomBubble( minSize: 8.0, maxSize: 25.0, limits: (1000, 50000), // Scale domain control ) ``` **Testing & Quality:** * 803 lines of new tests added * All 263 tests passing * Zero breaking changes * Production ready [View axis bounds documentation](/advanced/scales) ### 📊 Progress Bar Charts **Professional progress visualization with multiple styles!** * **Multiple Orientations**: Horizontal, vertical, and circular progress bars * **Advanced Styles**: Stacked, grouped, gauge, and concentric layouts * **Theme-Responsive**: Full dark mode and custom palette support * **Robust Validation**: Comprehensive input validation and error handling ```dart theme={null} // Stacked Progress Bar CristalyseChart() .data([ {'project': 'App', 'stage': 'Design', 'progress': 30.0}, {'project': 'App', 'stage': 'Development', 'progress': 50.0}, {'project': 'App', 'stage': 'Testing', 'progress': 20.0}, ]) .mappingProgress(category: 'project', value: 'progress', group: 'stage') .geomProgress( style: ProgressStyle.stacked, orientation: ProgressOrientation.horizontal, ) .build() ``` **Documentation Improvements:** * Enhanced SEO with comprehensive metadata * Custom 404 page with helpful navigation * Subscribable RSS feed for updates * Fixed all broken links * Improved contextual menu copy ### 🤖 MCP Server Integration **Cristalyse now integrates with AI coding assistants!** * New documentation guide for connecting Cristalyse docs to AI coding assistants (Cursor, Windsurf, Warp, Claude) * Enable AI assistants to access complete documentation, examples, and best practices directly in your IDE * Setup instructions: Add `"cristalyse_docs": {"url": "https://docs.cristalyse.com/mcp"}` to MCP settings [Learn more about MCP Server](/cristalyse-mcp-server) ### 🌈 Gradient Color Support (Experimental) **Transform your charts with stunning gradient effects!** * Category-specific gradients with `categoryGradients` property * Support for Linear, Radial, and Sweep gradients * Advanced alpha blending that respects animation transparency * Works with bar charts and scatter plots ```dart theme={null} CristalyseChart() .data(data) .mapping(x: 'quarter', y: 'revenue', color: 'quarter') .geomBar() .customPalette(categoryGradients: { 'Q1': LinearGradient(colors: [Colors.blue, Colors.cyan]), 'Q2': RadialGradient(colors: [Colors.red, Colors.orange]), }) .build() ``` ⚠️ **Note:** Not advisable for production use as of v1.6.0 ### 🔥 Built-In Legend Support **Professional legends with zero configuration!** * Simple `.legend()` method with smart defaults * 8 flexible positioning options (topLeft, topRight, bottom, etc.) * Automatic symbol generation based on chart type * Full dark mode support with theme-aware text colors ```dart theme={null} CristalyseChart() .data(salesData) .mapping(x: 'quarter', y: 'revenue', color: 'product') .geomBar(style: BarStyle.grouped) .legend() // That's it! ✨ .build() ``` [View legend documentation](/features/legends) ### 🎨 Custom Category Colors **Brand-consistent charts with custom color palettes!** * New `customPalette()` method for category-specific colors * Smart fallback system for unmapped categories * Perfect for corporate dashboards and brand consistency ```dart theme={null} final platformColors = { 'iOS': const Color(0xFF007ACC), // Apple Blue 'Android': const Color(0xFF3DDC84), // Android Green 'Web': const Color(0xFFFF6B35), // Web Orange }; CristalyseChart() .customPalette(categoryColors: platformColors) .build() ``` ### 🐛 Multi-Series Line Chart Fixes * Fixed critical rendering issues with multi-series line charts * Resolved missing data points on multi-series visualizations * Fixed overlapping series lines for better visual separation ### 🫧 Bubble Chart Support **Three-dimensional data visualization is here!** * Full bubble chart implementation with size mapping * Advanced `SizeScale` class for proportional bubble sizing * Interactive tooltips with rich hover information * New `geomBubble()` API following grammar of graphics ```dart theme={null} CristalyseChart() .data(companyData) .mapping( x: 'revenue', y: 'customers', size: 'marketShare', color: 'category', ) .geomBubble( minSize: 8.0, maxSize: 25.0, alpha: 0.75, ) .build() ``` [View bubble chart documentation](/charts/bubble-charts) ### Bug Fixes & Improvements * Fixed heatmap cell ordering to match axis labels * Fixed horizontal grouped bar charts crash * Fixed heatmap alpha calculation overflow * Improved code quality with comprehensive docstrings ### 🎨 Enhanced HeatMap Text Readability * Improved text visibility for low-value cells * Values \< 15% now display with black text for guaranteed readability * Values ≥ 15% use smart brightness-based contrast * Zero breaking changes - fully backward compatible ### 🔥 Heat Map Chart Support **Visualize 2D data patterns with professional heat maps!** * Comprehensive heat map implementation with customizable styling * Advanced color mapping with smooth gradients * Wave-effect animations with staggered cell appearance * Smart value visualization with automatic contrast detection ```dart theme={null} CristalyseChart() .data(salesData) .mappingHeatMap(x: 'month', y: 'region', value: 'revenue') .geomHeatMap( cellSpacing: 2.0, colorGradient: [Colors.red, Colors.yellow, Colors.green], showValues: true, ) .build() ``` [View heat map documentation](/charts/heat-map-charts) ### 🎯 Advanced Label Formatting **Professional data visualization with NumberFormat integration!** * Full callback-based label formatting system * Seamless integration with Flutter's `intl` package * Currency, percentages, compact notation support ```dart theme={null} CristalyseChart() .scaleYContinuous( labels: NumberFormat.simpleCurrency().format // $1,234.56 ) .build() ``` 🙏 **Feature authored by [@davidlrichmond](https://github.com/davidlrichmond)** ### Fixed * **Grouped Bar Chart Alignment**: Fixed positioning of grouped bars on ordinal scales * Bars now center properly on tick marks * Thanks [@davidlrichmond](https://github.com/davidlrichmond)! ### 🥧 Pie & Donut Charts **Major v1.0 release with comprehensive pie chart support!** * Full pie chart and donut chart implementation * Smooth slice animations with staggered timing * Smart label positioning with percentage display * Exploded slice functionality for emphasis * New `.mappingPie()` and `.geomPie()` API ```dart theme={null} CristalyseChart() .mappingPie(value: 'revenue', category: 'department') .geomPie( outerRadius: 120.0, innerRadius: 60.0, // Creates donut showLabels: true, ) .build() ``` [View pie chart documentation](/charts/pie-charts) ### 📖 Documentation Site Launch * **[docs.cristalyse.com](https://docs.cristalyse.com)** is now live! * Comprehensive guides, examples, and API reference * Improved web WASM compatibility ### Advanced Pan Control System * Fixed chart position reset bug * Infinite panning capability in any direction * Visual clipping implementation for clean boundaries * Selective axis panning with `updateXDomain` and `updateYDomain` Perfect for exploring large datasets! ### Enhanced SVG Export * Professional-quality vector graphics output * Support for all chart types * Perfect for presentations and reports * Editable in Figma, Adobe Illustrator, etc. ### 🎨 Area Chart Support **Visualize volume and trends with area charts!** * Comprehensive `AreaGeometry` with customizable styling * Progressive area animations * Multi-series support with transparency * Dual Y-axis compatibility ```dart theme={null} CristalyseChart() .geomArea( strokeWidth: 2.0, alpha: 0.3, fillArea: true, ) .build() ``` [View area chart documentation](/charts/area-charts) ### Interactive Panning System * Persistent pan state across gestures * Real-time visible range synchronization * Comprehensive `PanConfig` API with callbacks * Perfect for time series data exploration ### 🎯 Interactive Chart Layer **Tooltips, hover, and click interactions!** * New interaction system for user engagement * Flexible tooltip system with `TooltipConfig` * `onHover`, `onExit`, and `onTap` callbacks ```dart theme={null} CristalyseChart() .interaction( tooltip: TooltipConfig( builder: (point) => MyCustomTooltip(point: point), ), click: ClickConfig( onTap: (point) => showDetails(point), ), ) .build() ``` [View interaction documentation](/features/interactions) ### 🚀 Dual Y-Axis Support **Professional business dashboards unlocked!** * Independent left and right Y-axes * New `.mappingY2()` and `.scaleY2Continuous()` methods * Perfect for Revenue vs Conversion Rate charts * Fixed ordinal scale support for lines and points ```dart theme={null} CristalyseChart() .mapping(x: 'month', y: 'revenue') .mappingY2('conversion_rate') .geomBar(yAxis: YAxis.primary) .geomLine(yAxis: YAxis.secondary) .scaleY2Continuous(min: 0, max: 100) .build() ``` [View dual axis documentation](/charts/dual-axis) ### Bar Charts & Theming * **Stacked Bar Charts**: Full support with progressive animations * **Enhanced Theming**: Solarized Light/Dark themes * **Color Palettes**: Warm, cool, and pastel options * **Horizontal Bars**: Via `coordFlip()` method ### Line Charts & Animations * Line chart support with `geomLine()` * Configurable animations with curves * Multi-series support with color grouping * Progressive line drawing animations * Dark theme support ### 🎉 Initial Release **Cristalyse is born!** * Basic scatter plot support * Grammar of graphics API * Linear scales for continuous data * Light and dark themes * Cross-platform Flutter support