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
  .scaleYContinuous(min: 0, max: 200) // Y-axis
  .geomLine()
  .build()

Configuration

  • Domain: Data range mapped to chart space.
  • Range: Pixel space where the data is plotted.
  • Invertible: Values can map back to the data space.

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()

Configuration

  • Domain: List of categories.
  • Range: Space for categories to be displayed.
  • BandWidth: Space allocated to each category.

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()

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:
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()

Conditional Formatting (Value-Based Logic)

// 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)

// 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

// 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.

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:
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()

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.