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:
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:
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:
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:
// 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:
CristalyseChart ()
. data (data)
. mapping (x : 'category' , y : 'value' )
. scaleXOrdinal () // X-axis for categories
. geomBar ()
. build ()
With Axis Title:
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:
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:
CristalyseChart ()
. data (data)
. mapping (x : 'x' , y : 'y' , size : 'value' )
. geomPoint ()
. build ()
Transform axis labels by passing a callback to the labels parameter on any axis scale that takes any number and returns a string.
For simple cases, you can use NumberFormat from Dart’s intl package:
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.
// 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 ()
// 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 ()
// 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 ()
// 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 ()
Scale Customization
Customize scales to fit your data representation needs:
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.
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:
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:
CristalyseChart ()
. data (dynamicData)
. mapping (x : 'date' , y : 'value' )
. scaleXContinuous (min : startDate, max : endDate)
. scaleYContinuous (min : minValue, max : maxValue)
. geomArea ()
. build ()
Example Gallery
Time-Series Analysis Vivid depiction of temporal trends and changes over time.
Categorical Comparison Clear comparison of discrete categories using ordinal scaling.
Revenue vs. Conversion Dual-axis chart illustrating financial performance metrics.
Dynamic Data Scaling Interactive scales adapting to real-time data changes.
Next Steps
Technical Details
Review the source code behind scale transformations and extend as needed within the scale.dart file.