cristalyse 0.4.2
cristalyse: ^0.4.2 copied to clipboard
Cristalyse is a high-performance data visualization library for Dart/Flutter that implements grammar of graphics principles with native rendering capabilities.
example/lib/main.dart
import 'dart:math' as math;
import 'package:cristalyse/cristalyse.dart';
import 'package:cristalyse_example/chartTheme.dart';
import 'package:flutter/material.dart';
import 'graphs/barChart.dart';
import 'graphs/groupedBar.dart';
import 'graphs/horizontalBarChart.dart';
import 'graphs/lineChart.dart';
import 'graphs/scatterPlot.dart';
void main() {
runApp(CristalyseExampleApp());
}
class CristalyseExampleApp extends StatelessWidget {
const CristalyseExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cristalyse - Grammar of Graphics for Flutter',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
fontFamily: 'SF Pro Display',
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2563EB),
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
fontFamily: 'SF Pro Display',
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF3B82F6),
brightness: Brightness.dark,
),
),
home: ExampleHome(),
);
}
}
class ExampleHome extends StatefulWidget {
const ExampleHome({super.key});
@override
State<ExampleHome> createState() => _ExampleHomeState();
}
class _ExampleHomeState extends State<ExampleHome>
with TickerProviderStateMixin {
late TabController _tabController;
late AnimationController _fabAnimationController;
late Animation<double> _fabAnimation;
int _currentThemeIndex = 0;
final _themes = [
ChartTheme.defaultTheme(),
ChartTheme.darkTheme(),
ChartTheme.solarizedLightTheme(),
ChartTheme.solarizedDarkTheme(),
];
final _themeNames = ['Default', 'Dark', 'Solarized Light', 'Solarized Dark'];
int _currentPaletteIndex = 0;
final _colorPalettes = [
ChartTheme.defaultTheme().colorPalette,
const [
Color(0xfff44336),
Color(0xffe91e63),
Color(0xff9c27b0),
Color(0xff673ab7)
],
const [
Color(0xff2196f3),
Color(0xff00bcd4),
Color(0xff009688),
Color(0xff4caf50)
],
const [
Color(0xffffb74d),
Color(0xffff8a65),
Color(0xffdce775),
Color(0xffaed581)
],
];
final _paletteNames = ['Default', 'Warm', 'Cool', 'Pastel'];
double _sliderValue = 0.5;
bool _showControls = false;
late final List<Map<String, dynamic>> _scatterPlotData;
late final List<Map<String, dynamic>> _lineChartData;
late final List<Map<String, dynamic>> _barChartData;
late final List<Map<String, dynamic>> _groupedBarData;
late final List<Map<String, dynamic>> _horizontalBarData;
@override
void initState() {
super.initState();
_tabController = TabController(length: 5, vsync: this);
_fabAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fabAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _fabAnimationController, curve: Curves.elasticOut),
);
_tabController.addListener(() {
if (mounted) setState(() {});
});
_generateSampleData();
_fabAnimationController.forward();
}
void _generateSampleData() {
// More realistic scatter plot data - Sales Performance
_scatterPlotData = List.generate(60, (i) {
final x = i.toDouble();
final baseY = 20 + x * 0.8 + math.sin(x * 0.1) * 15;
final noise = (math.Random().nextDouble() - 0.5) * 12;
final y = math.max(5, baseY + noise);
final categories = ['Enterprise', 'SMB', 'Startup'];
final category = categories[i % 3];
final size = 1.0 + (y / 10).clamp(0, 8);
return {'x': x, 'y': y, 'category': category, 'size': size};
});
// Realistic line chart - User Growth
_lineChartData = List.generate(24, (i) {
final x = i.toDouble();
final baseGrowth = 50 + i * 3.2;
final seasonal = math.sin(x * 0.5) * 8;
final y = baseGrowth + seasonal + (math.Random().nextDouble() - 0.5) * 6;
return {'x': x, 'y': y, 'category': 'Monthly Active Users (k)'};
});
// Realistic bar chart - Quarterly Revenue
final quarters = ['Q1 2024', 'Q2 2024', 'Q3 2024', 'Q4 2024'];
_barChartData = quarters.asMap().entries.map((entry) {
final revenue = 120 + entry.key * 25 + math.Random().nextDouble() * 20;
return {'quarter': entry.value, 'revenue': revenue};
}).toList();
// Realistic grouped bar data - Product Performance
final products = ['Mobile App', 'Web Platform', 'API Services'];
final groupedQuarters = ['Q1', 'Q2', 'Q3', 'Q4'];
_groupedBarData = <Map<String, dynamic>>[];
for (final quarter in groupedQuarters) {
for (int i = 0; i < products.length; i++) {
final baseRevenue = 30 + groupedQuarters.indexOf(quarter) * 8;
final productMultiplier = [1.2, 0.9, 0.7][i];
final revenue =
baseRevenue * productMultiplier + math.Random().nextDouble() * 15;
_groupedBarData.add({
'quarter': quarter,
'product': products[i],
'revenue': revenue,
});
}
}
// Realistic horizontal bar data - Team Performance
final departments = [
'Engineering',
'Product',
'Sales',
'Marketing',
'Customer Success'
];
_horizontalBarData = departments.asMap().entries.map((entry) {
final multipliers = [1.0, 0.8, 0.9, 0.7, 0.6];
final headcount = 25 + (entry.key * 8) + math.Random().nextDouble() * 12;
return {
'department': entry.value,
'headcount': headcount * multipliers[entry.key]
};
}).toList();
}
@override
void dispose() {
_tabController.dispose();
_fabAnimationController.dispose();
super.dispose();
}
ChartTheme get currentTheme {
final baseTheme = _themes[_currentThemeIndex];
return baseTheme.copyWith(
colorPalette: _colorPalettes[_currentPaletteIndex],
);
}
String _getDisplayedValue() {
final index = _tabController.index;
switch (index) {
case 0:
final value = 2.0 + _sliderValue * 20.0;
return 'Point Size: ${value.toStringAsFixed(1)}px';
case 1:
final value = 1.0 + _sliderValue * 9.0;
return 'Line Width: ${value.toStringAsFixed(1)}px';
case 2:
case 3:
case 4:
final value = _sliderValue.clamp(0.1, 1.0);
return 'Bar Width: ${(value * 100).toStringAsFixed(0)}%';
default:
return _sliderValue.toStringAsFixed(2);
}
}
List<String> _getChartTitles() {
return [
'Sales Performance Analysis',
'User Growth Trends',
'Quarterly Revenue',
'Product Performance by Quarter',
'Team Size by Department'
];
}
List<String> _getChartDescriptions() {
return [
'Enterprise clients show higher deal values with consistent growth patterns',
'Steady monthly growth with seasonal variations in user acquisition',
'Strong Q4 performance driven by holiday sales and new partnerships',
'Mobile app leading growth, API services showing steady adoption',
'Engineering team expansion supporting our product development goals'
];
}
Widget _buildStatsCard(
String title, String value, String change, Color color) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withValues(alpha: 0.2)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Colors.grey[600],
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
change,
style: TextStyle(
fontSize: 9,
color: Colors.green[600],
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildControlPanel() {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: _showControls ? null : 0,
child: _showControls
? Container(
margin: const EdgeInsets.fromLTRB(16, 8, 16, 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.tune,
color: Theme.of(context).primaryColor, size: 18),
const SizedBox(width: 8),
Text(
'Chart Controls',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
const Spacer(),
IconButton(
onPressed: () => setState(() => _showControls = false),
icon: const Icon(Icons.keyboard_arrow_up),
iconSize: 18,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_getDisplayedValue(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
SliderTheme(
data: SliderThemeData(
trackHeight: 3,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6,
),
overlayShape: const RoundSliderOverlayShape(
overlayRadius: 12,
),
),
child: Slider(
value: _sliderValue,
min: 0.0,
max: 1.0,
divisions: 20,
onChanged: (value) =>
setState(() => _sliderValue = value),
),
),
],
),
),
const SizedBox(width: 20),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${_themeNames[_currentThemeIndex]} • ${_paletteNames[_currentPaletteIndex]}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.grey[700],
),
),
const SizedBox(height: 6),
Row(
children: _colorPalettes[_currentPaletteIndex]
.take(4)
.map((color) => Container(
width: 16,
height: 16,
margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 1.5,
),
),
))
.toList(),
),
],
),
],
),
],
),
)
: const SizedBox.shrink(),
);
}
@override
Widget build(BuildContext context) {
final chartTitles = _getChartTitles();
final chartDescriptions = _getChartDescriptions();
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'🔮 Cristalyse',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
Text(
'Grammar of Graphics for Flutter',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.normal,
),
),
],
),
actions: [
IconButton(
onPressed: () => setState(() => _showControls = !_showControls),
icon: Icon(_showControls ? Icons.visibility_off : Icons.visibility),
),
],
bottom: TabBar(
controller: _tabController,
isScrollable: true,
tabAlignment: TabAlignment.start,
indicatorWeight: 3,
labelStyle: const TextStyle(fontWeight: FontWeight.w600),
tabs: const [
Tab(text: 'Scatter Plot'),
Tab(text: 'Line Chart'),
Tab(text: 'Bar Chart'),
Tab(text: 'Grouped Bars'),
Tab(text: 'Horizontal Bars'),
],
),
),
body: Column(
children: [
_buildControlPanel(),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildChartPage(
chartTitles[0],
chartDescriptions[0],
buildScatterPlotTab(
currentTheme, _scatterPlotData, _sliderValue),
[
_buildStatsCard(
'Avg Deal Size', '\$47.2k', '+12.3%', Colors.blue),
_buildStatsCard(
'Conversion Rate', '23.4%', '+2.1%', Colors.green),
_buildStatsCard(
'Total Deals', '156', '+8.9%', Colors.orange),
],
),
_buildChartPage(
chartTitles[1],
chartDescriptions[1],
buildLineChartTab(currentTheme, _lineChartData, _sliderValue),
[
_buildStatsCard(
'Monthly Growth', '3.2%', '+0.4%', Colors.green),
_buildStatsCard(
'Active Users', '89.4k', '+15.2%', Colors.blue),
_buildStatsCard(
'Retention', '87.2%', '+1.8%', Colors.purple),
],
),
_buildChartPage(
chartTitles[2],
chartDescriptions[2],
buildBarChartTab(currentTheme, _barChartData, _sliderValue),
[
_buildStatsCard(
'Q4 Revenue', '\$1.2M', '+24.7%', Colors.green),
_buildStatsCard(
'YoY Growth', '31.5%', '+5.2%', Colors.blue),
_buildStatsCard(
'Profit Margin', '18.3%', '+2.1%', Colors.orange),
],
),
_buildChartPage(
chartTitles[3],
chartDescriptions[3],
buildGroupedBarTab(
currentTheme, _groupedBarData, _sliderValue),
[
_buildStatsCard(
'Mobile Revenue', '\$450k', '+18.2%', Colors.blue),
_buildStatsCard(
'Web Platform', '\$320k', '+12.4%', Colors.green),
_buildStatsCard(
'API Services', '\$180k', '+8.7%', Colors.orange),
],
),
_buildChartPage(
chartTitles[4],
chartDescriptions[4],
buildHorizontalBarTab(
currentTheme, _horizontalBarData, _sliderValue),
[
_buildStatsCard('Total Team', '127', '+12', Colors.blue),
_buildStatsCard(
'Eng Growth', '23.5%', '+3.2%', Colors.green),
_buildStatsCard(
'Avg Tenure', '2.8y', '+0.3y', Colors.purple),
],
),
],
),
),
],
),
floatingActionButton: AnimatedBuilder(
animation: _fabAnimation,
builder: (context, child) {
return Transform.scale(
scale: _fabAnimation.value,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton.extended(
onPressed: () {
setState(() {
_currentThemeIndex =
(_currentThemeIndex + 1) % _themes.length;
});
},
icon: const Icon(Icons.palette),
label: Text(_themeNames[_currentThemeIndex]),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
),
const SizedBox(width: 16),
FloatingActionButton(
onPressed: () {
setState(() {
_currentPaletteIndex =
(_currentPaletteIndex + 1) % _colorPalettes.length;
});
},
child: const Icon(Icons.color_lens),
backgroundColor: _colorPalettes[_currentPaletteIndex].first,
foregroundColor: Colors.white,
),
],
),
);
},
),
);
}
Widget _buildChartPage(
String title, String description, Widget chart, List<Widget> stats) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
description,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
height: 1.3,
),
),
const SizedBox(height: 16),
// Stats Row
Row(
children: stats
.map((stat) => Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 8),
child: stat,
),
))
.toList(),
),
const SizedBox(height: 16),
// Chart Container
Container(
height: 380,
width: double.infinity,
decoration: BoxDecoration(
color: currentTheme.backgroundColor,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: chart,
),
),
const SizedBox(height: 16),
// Features section
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(context).dividerColor.withValues(alpha: 0.1),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.auto_awesome,
color: Theme.of(context).primaryColor,
size: 18,
),
const SizedBox(width: 8),
Text(
'Chart Features',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
],
),
const SizedBox(height: 10),
_buildFeatureList(),
],
),
),
],
),
);
}
Widget _buildFeatureList() {
final features = _getChartFeatures();
return Column(
children: features
.map((feature) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Container(
width: 4,
height: 4,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
feature,
style: TextStyle(
fontSize: 13,
color: Colors.grey[700],
height: 1.3,
),
),
),
],
),
))
.toList(),
);
}
List<String> _getChartFeatures() {
switch (_tabController.index) {
case 0:
return [
'Grammar of graphics API with intuitive data mapping',
'Smooth 60fps animations with elastic curves',
'Size and color encoding for multi-dimensional data',
'Responsive scaling and high-DPI support'
];
case 1:
return [
'Progressive line drawing with smooth transitions',
'Multi-series support with automatic color mapping',
'Customizable stroke width and transparency',
'Optimized for time-series and continuous data'
];
case 2:
return [
'Categorical data visualization with ordinal scales',
'Staggered bar animations for visual impact',
'Automatic baseline detection and scaling',
'Customizable bar width and styling options'
];
case 3:
return [
'Side-by-side comparison of multiple data series',
'Coordinated group animations with smooth timing',
'Automatic legend generation from color mappings',
'Perfect for product or regional comparisons'
];
case 4:
return [
'Coordinate system flipping for horizontal layouts',
'Ideal for ranking and categorical comparisons',
'Space-efficient labeling for long category names',
'Consistent animation system across orientations'
];
default:
return [];
}
}
}