blob: e4a9948616d896e251d8160132bf3729b09533be [file] [log] [blame]
//
// Copyright 2014 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
//
/// Collection of scales for use by charts. The role of scales is to map the
/// input domain to an output range.
///
/// Charted supports two types of scales:
/// - Quantitative, where the scales use a mathematical function for mapping
/// the input domain to output range.
/// - Ordinal, where the input domain is discrete (i.e set of values)
///
library charted.core.scales;
import 'dart:math' as math;
import 'package:charted/core/utils.dart';
import 'package:charted/core/interpolators.dart';
import 'package:charted/core/time_interval.dart';
import 'package:charted/locale/locale.dart';
import 'package:charted/locale/format.dart';
part 'scales/ordinal_scale.dart';
part 'scales/linear_scale.dart';
part 'scales/log_scale.dart';
part 'scales/time_scale.dart';
typedef num RoundFunction(num value);
/// Minimum common interface supported by all scales. [QuantitativeScale] and
/// [OrdinalScale] contain the interface for their respective types.
abstract class Scale {
/// Given a [value] in the input domain, map it to the range.
/// On [QuantitativeScale]s both parameter and return values are numbers.
dynamic scale(value);
/// Given a [value] in the output range, return value in the input domain
/// that maps to the value.
/// On [QuantitativeScale]s both parameter and return values are numbers.
dynamic invert(value);
/// Input domain used by this scale.
Iterable domain;
/// Output range used by this scale.
Iterable range;
/// Maximum and minimum values of the scale's output range.
Extent get rangeExtent;
/// Creates tick values over the input domain.
Iterable get ticks;
/// Creates a formatter that is suitable for formatting the ticks.
/// For ordinal scale, the returned function is an identity function.
FormatFunction createTickFormatter([String format]);
/// Creates a clone of this scale.
Scale clone();
/// Suggested number of ticks on this scale.
/// Note: This property is only valid on quantitative scales.
int ticksCount;
/// Indicates if the current scale is using niced values for ticks.
/// Note: This property is only valid on quantitative scales.
bool nice;
/// Indicates if output range is clamped. When clamp is not true, any input
/// value that is not within the input domain may result in a value that is
/// outside the output range.
/// Note: This property is only valid on quantitative scales.
bool clamp;
/// Indicates that the scaled values must be rounded to the nearest
/// integer. Helps avoid anti-aliasing artifacts in the visualizations.
/// Note: This property is only valid on quantitative scales.
bool rounded;
}
/// Minimum common interface supported by scales whose input domain
/// contains discreet values (Ordinal scales).
abstract class OrdinalScale extends Scale {
factory OrdinalScale() = _OrdinalScale;
/// Amount of space that each value in the domain gets from the range. A band
/// is available only after [rangeBands] or [rangeRoundBands] is called by
/// the user. A bar-chart could use this space as width of a bar.
num get rangeBand;
/// Maps each value on the domain to a single point on output range. When a
/// non-zero value is specified, [padding] space is left unused on both ends
/// of the range.
void rangePoints(Iterable range, [double padding]);
/// Maps each value on the domain to a band in the output range. When a
/// non-zero value is specified, [padding] space is left between each bands
/// and [outerPadding] space is left unused at both ends of the range.
void rangeBands(Iterable range, [double padding, double outerPadding]);
/// Similar to [rangeBands] but ensures that each band starts and ends on a
/// pixel boundary - helps avoid anti-aliasing artifacts.
void rangeRoundBands(Iterable range, [double padding, double outerPadding]);
}
class RoundingFunctions extends Pair<RoundFunction, RoundFunction> {
RoundingFunctions(RoundFunction floor, RoundFunction ceil)
: super(floor, ceil);
factory RoundingFunctions.defaults() =>
new RoundingFunctions((x) => x.floor(), (x) => x.ceil());
factory RoundingFunctions.identity() =>
new RoundingFunctions(identityFunction, identityFunction);
RoundFunction get floor => super.first;
RoundFunction get ceil => super.last;
}
/// Namespacing container for utilities used by scales.
abstract class ScaleUtils {
/// Utility to return extent of sorted [values].
static Extent extent(Iterable values) => values.first < values.last
? new Extent(values.first, values.last)
: new Extent(values.last, values.first);
/// Extends [values] to round numbers based on the given pair of
/// floor and ceil functions. [functions] is a pair of rounding function
/// among which the first is used to compute floor of a number and the
/// second for ceil of the number.
static List nice(List values, RoundingFunctions functions) {
if (values.last >= values.first) {
values[0] = functions.floor(values.first);
values[values.length - 1] = functions.ceil(values.last);
} else {
values[values.length - 1] = functions.floor(values.last);
values[0] = functions.ceil(values.first);
}
return values;
}
static RoundingFunctions niceStep(num step) => (step > 0)
? new RoundingFunctions(
(x) => (x < step) ? x.floor() : (x / step).floor() * step,
(x) => (x < step) ? x.ceil() : (x / step).ceil() * step)
: new RoundingFunctions.identity();
/// Returns a Function that given a value x on the domain, returns the
/// corresponding value on the range on a bilinear scale.
///
/// @param domain The domain of the scale.
/// @param range The range of the scale.
/// @param uninterpolator The uninterpolator for domain values.
/// @param interpolator The interpolator for range values.
static Function bilinearScale(
List domain, List range, Function uninterpolator, Function interpolator) {
var u = uninterpolator(domain[0], domain[1]),
i = interpolator(range[0], range[1]);
return (x) => i(u(x));
}
/// Returns a Function that given a value x on the domain, returns the
/// corresponding value on the range on a polylinear scale.
///
/// @param domain The domain of the scale.
/// @param range The range of the scale.
/// @param uninterpolator The uninterpolator for domain values.
/// @param interpolator The interpolator for range values.
static Function polylinearScale(
List domain, List range, Function uninterpolator, Function interpolator) {
var u = [], i = [], j = 0, k = math.min(domain.length, range.length) - 1;
// Handle descending domains.
if (domain[k] < domain[0]) {
domain = domain.reversed.toList();
range = range.reversed.toList();
}
while (++j <= k) {
u.add(uninterpolator(domain[j - 1], domain[j]));
i.add(interpolator(range[j - 1], range[j]));
}
return (x) {
int index = bisect(domain, x, 1, k) - 1;
return i[index](u[index](x));
};
}
/// Returns the insertion point i for value x such that all values in a[lo:i]
/// will be less than x and all values in a[i:hi] will be equal to or greater
/// than x.
static int bisectLeft(List a, num x, [int lo = 0, int hi = -1]) {
if (hi == -1) {
hi = a.length;
}
while (lo < hi) {
int mid = ((lo + hi) / 2).floor();
if (a[mid] < x) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo;
}
/// Returns the insertion point i for value x such that all values in a[lo:i]
/// will be less than or equalto x and all values in a[i:hi] will be greater
/// than x.
static int bisectRight(List a, num x, [int lo = 0, int hi = -1]) {
if (hi == -1) {
hi = a.length;
}
while (lo < hi) {
int mid = ((lo + hi) / 2).floor();
if (x < a[mid]) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
static Function bisect = bisectRight;
}