1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-10 23:08:59 +00:00

Add flutter_web samples (#75)

This commit is contained in:
Kevin Moore
2019-05-07 13:32:08 -07:00
committed by Andrew Brogdon
parent 42f2dce01b
commit 3fe927cb29
697 changed files with 241026 additions and 0 deletions

View File

@@ -0,0 +1,240 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export 'src/chart/bar/bar_chart.dart' show BarChart;
export 'src/chart/bar/bar_label_decorator.dart'
show BarLabelAnchor, BarLabelDecorator, BarLabelPosition;
export 'src/chart/bar/bar_lane_renderer_config.dart' show BarLaneRendererConfig;
export 'src/chart/bar/bar_renderer.dart'
show BarRenderer, ImmutableBarRendererElement;
export 'src/chart/bar/bar_renderer_config.dart'
show
BarRendererConfig,
CornerStrategy,
ConstCornerStrategy,
NoCornerStrategy;
export 'src/chart/bar/bar_renderer_decorator.dart' show BarRendererDecorator;
export 'src/chart/bar/bar_target_line_renderer.dart' show BarTargetLineRenderer;
export 'src/chart/bar/bar_target_line_renderer_config.dart'
show BarTargetLineRendererConfig;
export 'src/chart/bar/base_bar_renderer_config.dart'
show BarGroupingType, BaseBarRendererConfig;
export 'src/chart/cartesian/axis/axis.dart'
show
domainAxisKey,
measureAxisIdKey,
measureAxisKey,
Axis,
NumericAxis,
OrdinalAxis,
OrdinalViewport;
export 'src/chart/cartesian/axis/numeric_extents.dart' show NumericExtents;
export 'src/chart/cartesian/axis/draw_strategy/gridline_draw_strategy.dart'
show GridlineRendererSpec;
export 'src/chart/cartesian/axis/draw_strategy/none_draw_strategy.dart'
show NoneRenderSpec;
export 'src/chart/cartesian/axis/draw_strategy/small_tick_draw_strategy.dart'
show SmallTickRendererSpec;
export 'src/chart/cartesian/axis/tick_formatter.dart'
show SimpleTickFormatterBase, TickFormatter;
export 'src/chart/cartesian/axis/spec/axis_spec.dart'
show
AxisSpec,
LineStyleSpec,
RenderSpec,
TextStyleSpec,
TickLabelAnchor,
TickLabelJustification,
TickFormatterSpec;
export 'src/chart/cartesian/axis/spec/bucketing_axis_spec.dart'
show BucketingAxisSpec, BucketingNumericTickProviderSpec;
export 'src/chart/cartesian/axis/spec/date_time_axis_spec.dart'
show
DateTimeAxisSpec,
DayTickProviderSpec,
AutoDateTimeTickFormatterSpec,
AutoDateTimeTickProviderSpec,
DateTimeEndPointsTickProviderSpec,
DateTimeTickFormatterSpec,
DateTimeTickProviderSpec,
TimeFormatterSpec,
StaticDateTimeTickProviderSpec;
export 'src/chart/cartesian/axis/spec/end_points_time_axis_spec.dart'
show EndPointsTimeAxisSpec;
export 'src/chart/cartesian/axis/spec/numeric_axis_spec.dart'
show
NumericAxisSpec,
NumericEndPointsTickProviderSpec,
NumericTickProviderSpec,
NumericTickFormatterSpec,
BasicNumericTickFormatterSpec,
BasicNumericTickProviderSpec,
StaticNumericTickProviderSpec;
export 'src/chart/cartesian/axis/spec/ordinal_axis_spec.dart'
show
BasicOrdinalTickProviderSpec,
BasicOrdinalTickFormatterSpec,
OrdinalAxisSpec,
OrdinalTickFormatterSpec,
OrdinalTickProviderSpec,
StaticOrdinalTickProviderSpec;
export 'src/chart/cartesian/axis/spec/percent_axis_spec.dart'
show PercentAxisSpec;
export 'src/chart/cartesian/axis/time/date_time_extents.dart'
show DateTimeExtents;
export 'src/chart/cartesian/axis/time/date_time_tick_formatter.dart'
show DateTimeTickFormatter;
export 'src/chart/cartesian/axis/spec/tick_spec.dart' show TickSpec;
export 'src/chart/cartesian/cartesian_chart.dart'
show CartesianChart, NumericCartesianChart, OrdinalCartesianChart;
export 'src/chart/cartesian/cartesian_renderer.dart' show BaseCartesianRenderer;
export 'src/chart/common/base_chart.dart' show BaseChart, LifecycleListener;
export 'src/chart/common/behavior/a11y/a11y_explore_behavior.dart'
show ExploreModeTrigger;
export 'src/chart/common/behavior/a11y/a11y_node.dart' show A11yNode;
export 'src/chart/common/behavior/a11y/domain_a11y_explore_behavior.dart'
show DomainA11yExploreBehavior, VocalizationCallback;
export 'src/chart/common/behavior/chart_behavior.dart'
show
BehaviorPosition,
ChartBehavior,
InsideJustification,
OutsideJustification;
export 'src/chart/common/behavior/calculation/percent_injector.dart'
show PercentInjector, PercentInjectorTotalType;
export 'src/chart/common/behavior/domain_highlighter.dart'
show DomainHighlighter;
export 'src/chart/common/behavior/initial_selection.dart' show InitialSelection;
export 'src/chart/common/behavior/legend/legend.dart'
show Legend, LegendCellPadding, LegendState, LegendTapHandling;
export 'src/chart/common/behavior/legend/legend_entry.dart' show LegendEntry;
export 'src/chart/common/behavior/legend/legend_entry_generator.dart'
show LegendEntryGenerator, LegendDefaultMeasure;
export 'src/chart/common/behavior/legend/datum_legend.dart' show DatumLegend;
export 'src/chart/common/behavior/legend/series_legend.dart' show SeriesLegend;
export 'src/chart/common/behavior/line_point_highlighter.dart'
show LinePointHighlighter, LinePointHighlighterFollowLineType;
export 'src/chart/common/behavior/range_annotation.dart'
show
AnnotationLabelAnchor,
AnnotationLabelDirection,
AnnotationLabelPosition,
AnnotationSegment,
LineAnnotationSegment,
RangeAnnotation,
RangeAnnotationAxisType,
RangeAnnotationSegment;
export 'src/chart/common/behavior/sliding_viewport.dart' show SlidingViewport;
export 'src/chart/common/behavior/chart_title/chart_title.dart'
show ChartTitle, ChartTitleDirection;
export 'src/chart/common/behavior/selection/lock_selection.dart'
show LockSelection;
export 'src/chart/common/behavior/selection/select_nearest.dart'
show SelectNearest;
export 'src/chart/common/behavior/selection/selection_trigger.dart'
show SelectionTrigger;
export 'src/chart/common/behavior/slider/slider.dart'
show
Slider,
SliderHandlePosition,
SliderListenerCallback,
SliderListenerDragState,
SliderStyle;
export 'src/chart/common/behavior/zoom/initial_hint_behavior.dart'
show InitialHintBehavior;
export 'src/chart/common/behavior/zoom/pan_and_zoom_behavior.dart'
show PanAndZoomBehavior;
export 'src/chart/common/behavior/zoom/pan_behavior.dart'
show PanBehavior, PanningCompletedCallback;
export 'src/chart/common/behavior/zoom/panning_tick_provider.dart'
show PanningTickProviderMode;
export 'src/chart/common/canvas_shapes.dart'
show CanvasBarStack, CanvasPie, CanvasPieSlice, CanvasRect;
export 'src/chart/common/chart_canvas.dart' show ChartCanvas, FillPatternType;
export 'src/chart/common/chart_context.dart' show ChartContext;
export 'src/chart/common/datum_details.dart'
show DatumDetails, DomainFormatter, MeasureFormatter;
export 'src/chart/common/processed_series.dart'
show ImmutableSeries, MutableSeries;
export 'src/chart/common/series_datum.dart' show SeriesDatum, SeriesDatumConfig;
export 'src/chart/common/selection_model/selection_model.dart'
show SelectionModel, SelectionModelType, SelectionModelListener;
export 'src/chart/common/series_renderer.dart'
show rendererIdKey, rendererKey, SeriesRenderer;
export 'src/chart/common/series_renderer_config.dart'
show RendererAttributeKey, SeriesRendererConfig;
export 'src/chart/layout/layout_config.dart' show LayoutConfig, MarginSpec;
export 'src/chart/layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
ViewMargin,
ViewMeasuredSizes;
export 'src/chart/line/line_chart.dart' show LineChart;
export 'src/chart/line/line_renderer.dart' show LineRenderer;
export 'src/chart/line/line_renderer_config.dart' show LineRendererConfig;
export 'src/chart/pie/arc_label_decorator.dart'
show ArcLabelDecorator, ArcLabelLeaderLineStyleSpec, ArcLabelPosition;
export 'src/chart/pie/arc_renderer.dart' show ArcRenderer;
export 'src/chart/pie/arc_renderer_config.dart' show ArcRendererConfig;
export 'src/chart/pie/pie_chart.dart' show PieChart;
export 'src/chart/scatter_plot/comparison_points_decorator.dart'
show ComparisonPointsDecorator;
export 'src/chart/scatter_plot/point_renderer.dart'
show
boundsLineRadiusPxKey,
boundsLineRadiusPxFnKey,
pointSymbolRendererFnKey,
pointSymbolRendererIdKey,
PointRenderer;
export 'src/chart/scatter_plot/point_renderer_config.dart'
show PointRendererConfig;
export 'src/chart/scatter_plot/point_renderer_decorator.dart'
show PointRendererDecorator;
export 'src/chart/scatter_plot/scatter_plot_chart.dart' show ScatterPlotChart;
export 'src/chart/scatter_plot/symbol_annotation_renderer.dart'
show SymbolAnnotationRenderer;
export 'src/chart/scatter_plot/symbol_annotation_renderer_config.dart'
show SymbolAnnotationRendererConfig;
export 'src/chart/time_series/time_series_chart.dart' show TimeSeriesChart;
export 'src/common/color.dart' show Color;
export 'src/common/date_time_factory.dart'
show DateTimeFactory, LocalDateTimeFactory, UTCDateTimeFactory;
export 'src/common/gesture_listener.dart' show GestureListener;
export 'src/common/graphics_factory.dart' show GraphicsFactory;
export 'src/common/line_style.dart' show LineStyle;
export 'src/common/material_palette.dart' show MaterialPalette;
export 'src/common/performance.dart' show Performance;
export 'src/common/proxy_gesture_listener.dart' show ProxyGestureListener;
export 'src/common/rtl_spec.dart' show AxisDirection, RTLSpec;
export 'src/common/style/material_style.dart' show MaterialStyle;
export 'src/common/style/style_factory.dart' show StyleFactory;
export 'src/common/symbol_renderer.dart'
show
CircleSymbolRenderer,
CylinderSymbolRenderer,
LineSymbolRenderer,
PointSymbolRenderer,
RectSymbolRenderer,
RoundedRectSymbolRenderer,
SymbolRenderer;
export 'src/common/text_element.dart'
show TextElement, TextDirection, MaxWidthStrategy;
export 'src/common/text_measurement.dart' show TextMeasurement;
export 'src/common/text_style.dart' show TextStyle;
export 'src/data/series.dart' show Series, TypedAccessorFn;

View File

@@ -0,0 +1,43 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap;
import '../bar/bar_renderer.dart' show BarRenderer;
import '../cartesian/axis/axis.dart' show NumericAxis;
import '../cartesian/cartesian_chart.dart' show OrdinalCartesianChart;
import '../common/series_renderer.dart' show SeriesRenderer;
import '../layout/layout_config.dart' show LayoutConfig;
class BarChart extends OrdinalCartesianChart {
BarChart(
{bool vertical,
LayoutConfig layoutConfig,
NumericAxis primaryMeasureAxis,
NumericAxis secondaryMeasureAxis,
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
: super(
vertical: vertical,
layoutConfig: layoutConfig,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes);
@override
SeriesRenderer<String> makeDefaultRenderer() {
return new BarRenderer<String>()
..rendererId = SeriesRenderer.defaultRendererId;
}
}

View File

@@ -0,0 +1,234 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:meta/meta.dart' show required;
import '../../common/color.dart' show Color;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/text_element.dart' show TextDirection;
import '../../common/text_style.dart' show TextStyle;
import '../../data/series.dart' show AccessorFn;
import '../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../common/chart_canvas.dart' show ChartCanvas;
import 'bar_renderer.dart' show ImmutableBarRendererElement;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
class BarLabelDecorator<D> extends BarRendererDecorator<D> {
// Default configuration
static const _defaultLabelPosition = BarLabelPosition.auto;
static const _defaultLabelPadding = 5;
static const _defaultLabelAnchor = BarLabelAnchor.start;
static final _defaultInsideLabelStyle =
new TextStyleSpec(fontSize: 12, color: Color.white);
static final _defaultOutsideLabelStyle =
new TextStyleSpec(fontSize: 12, color: Color.black);
/// Configures [TextStyleSpec] for labels placed inside the bars.
final TextStyleSpec insideLabelStyleSpec;
/// Configures [TextStyleSpec] for labels placed outside the bars.
final TextStyleSpec outsideLabelStyleSpec;
/// Configures where to place the label relative to the bars.
final BarLabelPosition labelPosition;
/// For labels drawn inside the bar, configures label anchor position.
final BarLabelAnchor labelAnchor;
/// Space before and after the label text.
final int labelPadding;
BarLabelDecorator(
{TextStyleSpec insideLabelStyleSpec,
TextStyleSpec outsideLabelStyleSpec,
this.labelPosition = _defaultLabelPosition,
this.labelPadding = _defaultLabelPadding,
this.labelAnchor = _defaultLabelAnchor})
: insideLabelStyleSpec = insideLabelStyleSpec ?? _defaultInsideLabelStyle,
outsideLabelStyleSpec =
outsideLabelStyleSpec ?? _defaultOutsideLabelStyle;
@override
void decorate(Iterable<ImmutableBarRendererElement<D>> barElements,
ChartCanvas canvas, GraphicsFactory graphicsFactory,
{@required Rectangle drawBounds,
@required double animationPercent,
@required bool renderingVertically,
bool rtl = false}) {
// TODO: Decorator not yet available for vertical charts.
assert(renderingVertically == false);
// Only decorate the bars when animation is at 100%.
if (animationPercent != 1.0) {
return;
}
// Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
// The [GraphicsFactory] is needed so it can't be created earlier.
final insideLabelStyle =
_getTextStyle(graphicsFactory, insideLabelStyleSpec);
final outsideLabelStyle =
_getTextStyle(graphicsFactory, outsideLabelStyleSpec);
for (var element in barElements) {
final labelFn = element.series.labelAccessorFn;
final datumIndex = element.index;
final label = (labelFn != null) ? labelFn(datumIndex) : null;
// If there are custom styles, use that instead of the default or the
// style defined for the entire decorator.
final datumInsideLabelStyle = _getDatumStyle(
element.series.insideLabelStyleAccessorFn,
datumIndex,
graphicsFactory,
defaultStyle: insideLabelStyle);
final datumOutsideLabelStyle = _getDatumStyle(
element.series.outsideLabelStyleAccessorFn,
datumIndex,
graphicsFactory,
defaultStyle: outsideLabelStyle);
// Skip calculation and drawing for this element if no label.
if (label == null || label.isEmpty) {
continue;
}
final bounds = element.bounds;
// Get space available inside and outside the bar.
final totalPadding = labelPadding * 2;
final insideBarWidth = bounds.width - totalPadding;
final outsideBarWidth = drawBounds.width - bounds.width - totalPadding;
final labelElement = graphicsFactory.createTextElement(label);
var calculatedLabelPosition = labelPosition;
if (calculatedLabelPosition == BarLabelPosition.auto) {
// For auto, first try to fit the text inside the bar.
labelElement.textStyle = datumInsideLabelStyle;
// A label fits if the space inside the bar is >= outside bar or if the
// length of the text fits and the space. This is because if the bar has
// more space than the outside, it makes more sense to place the label
// inside the bar, even if the entire label does not fit.
calculatedLabelPosition = (insideBarWidth >= outsideBarWidth ||
labelElement.measurement.horizontalSliceWidth < insideBarWidth)
? BarLabelPosition.inside
: BarLabelPosition.outside;
}
// Set the max width and text style.
if (calculatedLabelPosition == BarLabelPosition.inside) {
labelElement.textStyle = datumInsideLabelStyle;
labelElement.maxWidth = insideBarWidth;
} else {
// calculatedLabelPosition == LabelPosition.outside
labelElement.textStyle = datumOutsideLabelStyle;
labelElement.maxWidth = outsideBarWidth;
}
// Only calculate and draw label if there's actually space for the label.
if (labelElement.maxWidth > 0) {
// Calculate the start position of label based on [labelAnchor].
int labelX;
if (calculatedLabelPosition == BarLabelPosition.inside) {
switch (labelAnchor) {
case BarLabelAnchor.middle:
labelX = (bounds.left +
bounds.width / 2 -
labelElement.measurement.horizontalSliceWidth / 2)
.round();
labelElement.textDirection =
rtl ? TextDirection.rtl : TextDirection.ltr;
break;
case BarLabelAnchor.end:
case BarLabelAnchor.start:
final alignLeft = rtl
? (labelAnchor == BarLabelAnchor.end)
: (labelAnchor == BarLabelAnchor.start);
if (alignLeft) {
labelX = bounds.left + labelPadding;
labelElement.textDirection = TextDirection.ltr;
} else {
labelX = bounds.right - labelPadding;
labelElement.textDirection = TextDirection.rtl;
}
break;
}
} else {
// calculatedLabelPosition == LabelPosition.outside
labelX = bounds.right + labelPadding;
labelElement.textDirection = TextDirection.ltr;
}
// Center the label inside the bar.
final labelY = (bounds.top +
(bounds.bottom - bounds.top) / 2 -
labelElement.measurement.verticalSliceWidth / 2)
.round();
canvas.drawText(labelElement, labelX, labelY);
}
}
}
// Helper function that converts [TextStyleSpec] to [TextStyle].
TextStyle _getTextStyle(
GraphicsFactory graphicsFactory, TextStyleSpec labelSpec) {
return graphicsFactory.createTextPaint()
..color = labelSpec?.color ?? Color.black
..fontFamily = labelSpec?.fontFamily
..fontSize = labelSpec?.fontSize ?? 12;
}
/// Helper function to get datum specific style
TextStyle _getDatumStyle(AccessorFn<TextStyleSpec> labelFn, int datumIndex,
GraphicsFactory graphicsFactory,
{TextStyle defaultStyle}) {
final styleSpec = (labelFn != null) ? labelFn(datumIndex) : null;
return (styleSpec != null)
? _getTextStyle(graphicsFactory, styleSpec)
: defaultStyle;
}
}
/// Configures where to place the label relative to the bars.
enum BarLabelPosition {
/// Automatically try to place the label inside the bar first and place it on
/// the outside of the space available outside the bar is greater than space
/// available inside the bar.
auto,
/// Always place label on the outside.
outside,
/// Always place label on the inside.
inside,
}
/// Configures where to anchor the label for labels drawn inside the bars.
enum BarLabelAnchor {
/// Anchor to the measure start.
start,
/// Anchor to the middle of the measure range.
middle,
/// Anchor to the measure end.
end,
}

View File

@@ -0,0 +1,369 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap;
import '../../data/series.dart' show AttributeKey;
import '../cartesian/axis/axis.dart'
show ImmutableAxis, domainAxisKey, measureAxisKey;
import '../cartesian/cartesian_chart.dart' show CartesianChart;
import '../common/chart_canvas.dart' show ChartCanvas;
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
import 'bar_lane_renderer_config.dart' show BarLaneRendererConfig;
import 'bar_renderer.dart' show AnimatedBar, BarRenderer, BarRendererElement;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
import 'base_bar_renderer.dart'
show
barGroupCountKey,
barGroupIndexKey,
barGroupWeightKey,
previousBarGroupWeightKey,
stackKeyKey;
import 'base_bar_renderer_element.dart' show BaseBarRendererElement;
/// Key for storing a list of all domain values that exist in the series data.
///
/// In grouped stacked mode, this list will contain a combination of domain
/// value and series category.
const domainValuesKey = const AttributeKey<Set>('BarLaneRenderer.domainValues');
/// Renders series data as a series of bars with lanes.
///
/// Every stack of bars will have a swim lane rendered underneath the series
/// data, in a gray color by default. The swim lane occupies the same width as
/// the bar elements, and will be completely covered up if the bar stack happens
/// to take up the entire measure domain range.
///
/// If every bar that shares a domain value has a null measure value, then the
/// swim lanes may optionally be merged together into one wide lane that covers
/// the full domain range band width.
class BarLaneRenderer<D> extends BarRenderer<D> {
final BarRendererDecorator barRendererDecorator;
/// Store a map of domain+barGroupIndex+category index to bar lanes in a
/// stack.
///
/// This map is used to render all the bars in a stack together, to account
/// for rendering effects that need to take the full stack into account (e.g.
/// corner rounding).
///
/// [LinkedHashMap] is used to render the bars on the canvas in the same order
/// as the data was given to the chart. For the case where both grouping and
/// stacking are disabled, this means that bars for data later in the series
/// will be drawn "on top of" bars earlier in the series.
final _barLaneStackMap = new LinkedHashMap<String, List<AnimatedBar<D>>>();
/// Store a map of flags to track whether all measure values for a given
/// domain value are null, for every series on the chart.
final _allMeasuresForDomainNullMap = new LinkedHashMap<D, bool>();
factory BarLaneRenderer({BarLaneRendererConfig config, String rendererId}) {
rendererId ??= 'bar';
config ??= new BarLaneRendererConfig();
return new BarLaneRenderer._internal(
config: config, rendererId: rendererId);
}
BarLaneRenderer._internal({BarLaneRendererConfig config, String rendererId})
: barRendererDecorator = config.barRendererDecorator,
super.internal(config: config, rendererId: rendererId);
@override
void preprocessSeries(List<MutableSeries<D>> seriesList) {
super.preprocessSeries(seriesList);
_allMeasuresForDomainNullMap.clear();
seriesList.forEach((MutableSeries<D> series) {
final domainFn = series.domainFn;
final measureFn = series.rawMeasureFn;
final domainValues = new Set<D>();
for (var barIndex = 0; barIndex < series.data.length; barIndex++) {
final domain = domainFn(barIndex);
final measure = measureFn(barIndex);
domainValues.add(domain);
// Update the "all measure null" tracking for bars that have the
// current domain value.
if ((config as BarLaneRendererConfig).mergeEmptyLanes) {
final allNull = _allMeasuresForDomainNullMap[domain];
final isNull = measure == null;
_allMeasuresForDomainNullMap[domain] =
allNull != null ? allNull && isNull : isNull;
}
}
series.setAttr(domainValuesKey, domainValues);
});
}
@override
void update(List<ImmutableSeries<D>> seriesList, bool isAnimatingThisDraw) {
super.update(seriesList, isAnimatingThisDraw);
// Add gray bars to render under every bar stack.
seriesList.forEach((ImmutableSeries<D> series) {
Set<D> domainValues = series.getAttr(domainValuesKey) as Set<D>;
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final seriesStackKey = series.getAttr(stackKeyKey);
final barGroupCount = series.getAttr(barGroupCountKey);
final barGroupIndex = series.getAttr(barGroupIndexKey);
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
final barGroupWeight = series.getAttr(barGroupWeightKey);
final measureAxisPosition = measureAxis.getLocation(0.0);
final maxMeasureValue = _getMaxMeasureValue(measureAxis);
// Create a fake series for [BarLabelDecorator] to use when looking up the
// index of each datum.
final laneSeries = new MutableSeries<D>.clone(seriesList[0]);
laneSeries.data = [];
// Don't render any labels on the swim lanes.
laneSeries.labelAccessorFn = (int index) => '';
var laneSeriesIndex = 0;
domainValues.forEach((D domainValue) {
// Skip adding any background bars if they will be covered up by the
// domain-spanning null bar.
if (_allMeasuresForDomainNullMap[domainValue] == true) {
return;
}
// Add a fake datum to the series for [BarLabelDecorator].
final datum = {'index': laneSeriesIndex};
laneSeries.data.add(datum);
// Each bar should be stored in barStackMap in a structure that mirrors
// the visual rendering of the bars. Thus, they should be grouped by
// domain value, series category (by way of the stack keys that were
// generated for each series in the preprocess step), and bar group
// index to account for all combinations of grouping and stacking.
final barStackMapKey = domainValue.toString() +
'__' +
seriesStackKey +
'__' +
barGroupIndex.toString();
final barKey = barStackMapKey + '0';
final barStackList = _barLaneStackMap.putIfAbsent(
barStackMapKey, () => <AnimatedBar<D>>[]);
// If we already have an AnimatingBar for that index, use it.
var animatingBar = barStackList.firstWhere(
(AnimatedBar bar) => bar.key == barKey,
orElse: () => null);
// If we don't have any existing bar element, create a new bar and have
// it animate in from the domain axis.
if (animatingBar == null) {
animatingBar = makeAnimatedBar(
key: barKey,
series: laneSeries,
datum: datum,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: (config as BarLaneRendererConfig).backgroundBarColor,
details: new BarRendererElement<D>(),
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
measureValue: maxMeasureValue,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: config.strokeWidthPx,
measureIsNull: false,
measureIsNegative: false);
barStackList.add(animatingBar);
} else {
animatingBar
..datum = datum
..series = laneSeries
..domainValue = domainValue;
}
// Get the barElement we are going to setup.
// Optimization to prevent allocation in non-animating case.
BaseBarRendererElement barElement = makeBarRendererElement(
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: (config as BarLaneRendererConfig).backgroundBarColor,
details: new BarRendererElement<D>(),
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
measureValue: maxMeasureValue,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: config.strokeWidthPx,
measureIsNull: false,
measureIsNegative: false);
animatingBar.setNewTarget(barElement);
laneSeriesIndex++;
});
});
// Add domain-spanning bars to render when every measure value for every
// datum of a given domain is null.
if ((config as BarLaneRendererConfig).mergeEmptyLanes) {
// Use the axes from the first series.
final domainAxis =
seriesList[0].getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis =
seriesList[0].getAttr(measureAxisKey) as ImmutableAxis<num>;
final measureAxisPosition = measureAxis.getLocation(0.0);
final maxMeasureValue = _getMaxMeasureValue(measureAxis);
final barGroupIndex = 0;
final previousBarGroupWeight = 0.0;
final barGroupWeight = 1.0;
final barGroupCount = 1;
// Create a fake series for [BarLabelDecorator] to use when looking up the
// index of each datum. We don't care about any other series values for
// the merged lanes, so just clone the first series.
final mergedSeries = new MutableSeries<D>.clone(seriesList[0]);
mergedSeries.data = [];
// Add a label accessor that returns the empty lane label.
mergedSeries.labelAccessorFn =
(int index) => (config as BarLaneRendererConfig).emptyLaneLabel;
var mergedSeriesIndex = 0;
_allMeasuresForDomainNullMap.forEach((D domainValue, bool allNull) {
if (allNull) {
// Add a fake datum to the series for [BarLabelDecorator].
final datum = {'index': mergedSeriesIndex};
mergedSeries.data.add(datum);
final barStackMapKey = domainValue.toString() + '__allNull__';
final barKey = barStackMapKey + '0';
final barStackList = _barLaneStackMap.putIfAbsent(
barStackMapKey, () => <AnimatedBar<D>>[]);
// If we already have an AnimatingBar for that index, use it.
var animatingBar = barStackList.firstWhere(
(AnimatedBar bar) => bar.key == barKey,
orElse: () => null);
// If we don't have any existing bar element, create a new bar and have
// it animate in from the domain axis.
if (animatingBar == null) {
animatingBar = makeAnimatedBar(
key: barKey,
series: mergedSeries,
datum: datum,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: (config as BarLaneRendererConfig).backgroundBarColor,
details: new BarRendererElement<D>(),
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
measureValue: maxMeasureValue,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: config.strokeWidthPx,
measureIsNull: false,
measureIsNegative: false);
barStackList.add(animatingBar);
} else {
animatingBar
..datum = datum
..series = mergedSeries
..domainValue = domainValue;
}
// Get the barElement we are going to setup.
// Optimization to prevent allocation in non-animating case.
BaseBarRendererElement barElement = makeBarRendererElement(
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: (config as BarLaneRendererConfig).backgroundBarColor,
details: new BarRendererElement<D>(),
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
measureValue: maxMeasureValue,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: config.strokeWidthPx,
measureIsNull: false,
measureIsNegative: false);
animatingBar.setNewTarget(barElement);
mergedSeriesIndex++;
}
});
}
}
/// Gets the maximum measure value that will fit in the draw area.
num _getMaxMeasureValue(ImmutableAxis<num> measureAxis) {
final pos = (chart as CartesianChart).vertical
? chart.drawAreaBounds.top
: isRtl ? chart.drawAreaBounds.left : chart.drawAreaBounds.right;
return measureAxis.getDomain(pos.toDouble());
}
/// Paints the current bar data on the canvas.
@override
void paint(ChartCanvas canvas, double animationPercent) {
_barLaneStackMap.forEach((String stackKey, List<AnimatedBar<D>> barStack) {
// Turn this into a list so that the getCurrentBar isn't called more than
// once for each animationPercent if the barElements are iterated more
// than once.
List<BarRendererElement<D>> barElements = barStack
.map((AnimatedBar<D> animatingBar) =>
animatingBar.getCurrentBar(animationPercent))
.toList();
paintBar(canvas, animationPercent, barElements);
});
super.paint(canvas, animationPercent);
}
}

View File

@@ -0,0 +1,104 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../common/color.dart' show Color;
import '../../common/style/style_factory.dart' show StyleFactory;
import '../../common/symbol_renderer.dart';
import '../common/chart_canvas.dart' show FillPatternType;
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
import 'bar_label_decorator.dart' show BarLabelDecorator;
import 'bar_lane_renderer.dart' show BarLaneRenderer;
import 'bar_renderer_config.dart' show BarRendererConfig, CornerStrategy;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
import 'base_bar_renderer_config.dart' show BarGroupingType;
/// Configuration for a bar lane renderer.
class BarLaneRendererConfig extends BarRendererConfig<String> {
/// The color of background bars.
final Color backgroundBarColor;
/// Label text to draw on a merged empty lane.
///
/// This will only be drawn if all of the measures for a domain are null, and
/// [mergeEmptyLanes] is enabled.
///
/// The renderer must be configured with a [BarLabelDecorator] for this label
/// to be drawn.
final String emptyLaneLabel;
/// Whether or not all lanes for a given domain value should be merged into
/// one wide lane if all measure values for said domain are null.
final bool mergeEmptyLanes;
BarLaneRendererConfig({
String customRendererId,
CornerStrategy cornerStrategy,
this.emptyLaneLabel = 'No data',
FillPatternType fillPattern,
BarGroupingType groupingType,
int layoutPaintOrder = LayoutViewPaintOrder.bar,
this.mergeEmptyLanes = false,
int minBarLengthPx = 0,
double stackHorizontalSeparator,
double strokeWidthPx = 0.0,
BarRendererDecorator barRendererDecorator,
SymbolRenderer symbolRenderer,
Color backgroundBarColor,
List<int> weightPattern,
}) : backgroundBarColor =
backgroundBarColor ?? StyleFactory.style.noDataColor,
super(
barRendererDecorator: barRendererDecorator,
cornerStrategy: cornerStrategy,
customRendererId: customRendererId,
groupingType: groupingType ?? BarGroupingType.grouped,
layoutPaintOrder: layoutPaintOrder,
minBarLengthPx: minBarLengthPx,
fillPattern: fillPattern,
stackHorizontalSeparator: stackHorizontalSeparator,
strokeWidthPx: strokeWidthPx,
symbolRenderer: symbolRenderer,
weightPattern: weightPattern,
);
@override
BarLaneRenderer<String> build() {
return new BarLaneRenderer<String>(
config: this, rendererId: customRendererId);
}
@override
bool operator ==(other) {
if (identical(this, other)) {
return true;
}
if (!(other is BarLaneRendererConfig)) {
return false;
}
return other.backgroundBarColor == backgroundBarColor &&
other.emptyLaneLabel == emptyLaneLabel &&
other.mergeEmptyLanes == mergeEmptyLanes &&
super == (other);
}
@override
int get hashCode {
var hash = super.hashCode;
hash = hash * 31 + (backgroundBarColor?.hashCode ?? 0);
hash = hash * 31 + (emptyLaneLabel?.hashCode ?? 0);
hash = hash * 31 + (mergeEmptyLanes?.hashCode ?? 0);
return hash;
}
}

View File

@@ -0,0 +1,556 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show max, min, Point, Rectangle;
import 'package:meta/meta.dart' show protected, required;
import '../../common/color.dart' show Color;
import '../cartesian/axis/axis.dart'
show ImmutableAxis, domainAxisKey, measureAxisKey;
import '../common/canvas_shapes.dart' show CanvasBarStack, CanvasRect;
import '../common/chart_canvas.dart' show ChartCanvas, FillPatternType;
import '../common/datum_details.dart' show DatumDetails;
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
import '../common/series_datum.dart' show SeriesDatum;
import 'bar_renderer_config.dart' show BarRendererConfig, CornerStrategy;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
import 'base_bar_renderer.dart'
show
BaseBarRenderer,
barGroupCountKey,
barGroupIndexKey,
previousBarGroupWeightKey,
barGroupWeightKey;
import 'base_bar_renderer_element.dart'
show BaseAnimatedBar, BaseBarRendererElement;
/// Renders series data as a series of bars.
class BarRenderer<D>
extends BaseBarRenderer<D, BarRendererElement<D>, AnimatedBar<D>> {
/// If we are grouped, use this spacing between the bars in a group.
final _barGroupInnerPadding = 2;
/// The padding between bar stacks.
///
/// The padding comes out of the bottom of the bar.
final _stackedBarPadding = 1;
final BarRendererDecorator barRendererDecorator;
factory BarRenderer({BarRendererConfig config, String rendererId}) {
rendererId ??= 'bar';
config ??= new BarRendererConfig();
return new BarRenderer.internal(config: config, rendererId: rendererId);
}
/// This constructor is protected because it is used by child classes, which
/// cannot call the factory in their own constructors.
@protected
BarRenderer.internal({BarRendererConfig config, String rendererId})
: barRendererDecorator = config.barRendererDecorator,
super(
config: config,
rendererId: rendererId,
layoutPaintOrder: config.layoutPaintOrder);
@override
void configureSeries(List<MutableSeries<D>> seriesList) {
assignMissingColors(getOrderedSeriesList(seriesList),
emptyCategoryUsesSinglePalette: true);
}
DatumDetails<D> addPositionToDetailsForSeriesDatum(
DatumDetails<D> details, SeriesDatum<D> seriesDatum) {
final series = details.series;
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final barGroupIndex = series.getAttr(barGroupIndexKey);
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
final barGroupWeight = series.getAttr(barGroupWeightKey);
final numBarGroups = series.getAttr(barGroupCountKey);
final bounds = _getBarBounds(
details.domain,
domainAxis,
domainAxis.rangeBand.round(),
details.measure,
details.measureOffset,
measureAxis,
barGroupIndex,
previousBarGroupWeight,
barGroupWeight,
numBarGroups);
Point<double> chartPosition;
if (renderingVertically) {
chartPosition = new Point<double>(
(bounds.left + (bounds.width / 2)).toDouble(), bounds.top.toDouble());
} else {
chartPosition = new Point<double>(
isRtl ? bounds.left.toDouble() : bounds.right.toDouble(),
(bounds.top + (bounds.height / 2)).toDouble());
}
return new DatumDetails.from(details, chartPosition: chartPosition);
}
@override
BarRendererElement<D> getBaseDetails(dynamic datum, int index) {
return new BarRendererElement<D>();
}
CornerStrategy get cornerStrategy {
return (config as BarRendererConfig).cornerStrategy;
}
/// Generates an [AnimatedBar] to represent the previous and current state
/// of one bar on the chart.
@override
AnimatedBar<D> makeAnimatedBar(
{String key,
ImmutableSeries<D> series,
List<int> dashPattern,
dynamic datum,
Color color,
BarRendererElement<D> details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups,
bool measureIsNull,
bool measureIsNegative}) {
return new AnimatedBar<D>(
key: key, datum: datum, series: series, domainValue: domainValue)
..setNewTarget(makeBarRendererElement(
color: color,
dashPattern: dashPattern,
details: details,
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainWidth,
measureValue: measureValue,
measureOffsetValue: measureOffsetValue,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
fillColor: fillColor,
fillPattern: fillPattern,
strokeWidthPx: strokeWidthPx,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
numBarGroups: numBarGroups,
measureIsNull: measureIsNull,
measureIsNegative: measureIsNegative));
}
/// Generates a [BarRendererElement] to represent the rendering data for one
/// bar on the chart.
@override
BarRendererElement<D> makeBarRendererElement(
{Color color,
List<int> dashPattern,
BarRendererElement<D> details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups,
bool measureIsNull,
bool measureIsNegative}) {
return new BarRendererElement<D>()
..color = color
..dashPattern = dashPattern
..fillColor = fillColor
..fillPattern = fillPattern
..measureAxisPosition = measureAxisPosition
..roundPx = details.roundPx
..strokeWidthPx = strokeWidthPx
..measureIsNull = measureIsNull
..measureIsNegative = measureIsNegative
..bounds = _getBarBounds(
domainValue,
domainAxis,
domainWidth,
measureValue,
measureOffsetValue,
measureAxis,
barGroupIndex,
previousBarGroupWeight,
barGroupWeight,
numBarGroups);
}
@override
void paintBar(ChartCanvas canvas, double animationPercent,
Iterable<BarRendererElement<D>> barElements) {
final bars = <CanvasRect>[];
// When adjusting bars for stacked bar padding, do not modify the first bar
// if rendering vertically and do not modify the last bar if rendering
// horizontally.
final unmodifiedBar =
renderingVertically ? barElements.first : barElements.last;
// Find the max bar width from each segment to calculate corner radius.
int maxBarWidth = 0;
var measureIsNegative = false;
for (var bar in barElements) {
var bounds = bar.bounds;
measureIsNegative = measureIsNegative || bar.measureIsNegative;
if (bar != unmodifiedBar) {
bounds = renderingVertically
? new Rectangle<int>(
bar.bounds.left,
max(
0,
bar.bounds.top +
(measureIsNegative ? _stackedBarPadding : 0)),
bar.bounds.width,
max(0, bar.bounds.height - _stackedBarPadding),
)
: new Rectangle<int>(
max(
0,
bar.bounds.left +
(measureIsNegative ? _stackedBarPadding : 0)),
bar.bounds.top,
max(0, bar.bounds.width - _stackedBarPadding),
bar.bounds.height,
);
}
bars.add(new CanvasRect(bounds,
dashPattern: bar.dashPattern,
fill: bar.fillColor,
pattern: bar.fillPattern,
stroke: bar.color,
strokeWidthPx: bar.strokeWidthPx));
maxBarWidth = max(
maxBarWidth, (renderingVertically ? bounds.width : bounds.height));
}
bool roundTopLeft;
bool roundTopRight;
bool roundBottomLeft;
bool roundBottomRight;
if (measureIsNegative) {
// Negative bars should be rounded towards the negative axis direction.
// In vertical mode, this is the bottom. In horizontal mode, this is the
// left side of the chart for LTR, or the right side for RTL.
roundTopLeft = !renderingVertically && !isRtl ? true : false;
roundTopRight = !renderingVertically && isRtl ? true : false;
roundBottomLeft = renderingVertically || !isRtl ? true : false;
roundBottomRight = renderingVertically || isRtl ? true : false;
} else {
// Positive bars should be rounded towards the positive axis direction.
// In vertical mode, this is the top. In horizontal mode, this is the
// right side of the chart for LTR, or the left side for RTL.
roundTopLeft = renderingVertically || isRtl ? true : false;
roundTopRight = isRtl ? false : true;
roundBottomLeft = isRtl ? true : false;
roundBottomRight = renderingVertically || isRtl ? false : true;
}
final barStack = new CanvasBarStack(
bars,
radius: cornerStrategy.getRadius(maxBarWidth),
stackedBarPadding: _stackedBarPadding,
roundTopLeft: roundTopLeft,
roundTopRight: roundTopRight,
roundBottomLeft: roundBottomLeft,
roundBottomRight: roundBottomRight,
);
// If bar stack's range width is:
// * Within the component bounds, then draw the bar stack.
// * Partially out of component bounds, then clip the stack where it is out
// of bounds.
// * Fully out of component bounds, do not draw.
final barOutsideBounds = renderingVertically
? barStack.fullStackRect.left < componentBounds.left ||
barStack.fullStackRect.right > componentBounds.right
: barStack.fullStackRect.top < componentBounds.top ||
barStack.fullStackRect.bottom > componentBounds.bottom;
// TODO: When we have initial viewport, add image test for
// clipping.
if (barOutsideBounds) {
final clipBounds = _getBarStackBounds(barStack.fullStackRect);
// Do not draw the bar stack if it is completely outside of the component
// bounds.
if (clipBounds.width <= 0 || clipBounds.height <= 0) {
return;
}
canvas.setClipBounds(clipBounds);
}
canvas.drawBarStack(barStack, drawAreaBounds: componentBounds);
if (barOutsideBounds) {
canvas.resetClipBounds();
}
// Decorate the bar segments if there is a decorator.
barRendererDecorator?.decorate(barElements, canvas, graphicsFactory,
drawBounds: drawBounds,
animationPercent: animationPercent,
renderingVertically: renderingVertically,
rtl: isRtl);
}
/// Calculate the clipping region for a rectangle that represents the full bar
/// stack.
Rectangle<int> _getBarStackBounds(Rectangle<int> barStackRect) {
int left;
int right;
int top;
int bottom;
if (renderingVertically) {
// Only clip at the start and end so that the bar's width stays within
// the viewport, but any bar decorations above the bar can still show.
left = max(componentBounds.left, barStackRect.left);
right = min(componentBounds.right, barStackRect.right);
top = barStackRect.top;
bottom = barStackRect.bottom;
} else {
// Only clip at the top and bottom so that the bar's height stays within
// the viewport, but any bar decorations to the right of the bar can still
// show.
left = barStackRect.left;
right = barStackRect.right;
top = max(componentBounds.top, barStackRect.top);
bottom = min(componentBounds.bottom, barStackRect.bottom);
}
final width = right - left;
final height = bottom - top;
return new Rectangle(left, top, width, height);
}
/// Generates a set of bounds that describe a bar.
Rectangle<int> _getBarBounds(
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups) {
// If no weights were passed in, default to equal weight per bar.
if (barGroupWeight == null) {
barGroupWeight = 1 / numBarGroups;
previousBarGroupWeight = barGroupIndex * barGroupWeight;
}
// Calculate how wide each bar should be within the group of bars. If we
// only have one series, or are stacked, then barWidth should equal
// domainWidth.
int spacingLoss = (_barGroupInnerPadding * (numBarGroups - 1));
int barWidth = ((domainWidth - spacingLoss) * barGroupWeight).round();
// Make sure that bars are at least one pixel wide, so that they will always
// be visible on the chart. Ideally we should do something clever with the
// size of the chart, and the density and periodicity of the data, but this
// at least ensures that dense charts still have visible data.
barWidth = max(1, barWidth);
// Flip bar group index for calculating location on the domain axis if RTL.
final adjustedBarGroupIndex =
isRtl ? numBarGroups - barGroupIndex - 1 : barGroupIndex;
// Calculate the start and end of the bar, taking into account accumulated
// padding for grouped bars.
int previousAverageWidth = adjustedBarGroupIndex > 0
? ((domainWidth - spacingLoss) *
(previousBarGroupWeight / adjustedBarGroupIndex))
.round()
: 0;
int domainStart = (domainAxis.getLocation(domainValue) -
(domainWidth / 2) +
(previousAverageWidth + _barGroupInnerPadding) *
adjustedBarGroupIndex)
.round();
int domainEnd = domainStart + barWidth;
measureValue = measureValue != null ? measureValue : 0;
// Calculate measure locations. Stacked bars should have their
// offset calculated previously.
int measureStart;
int measureEnd;
if (measureValue < 0) {
measureEnd = measureAxis.getLocation(measureOffsetValue).round();
measureStart =
measureAxis.getLocation(measureValue + measureOffsetValue).round();
} else {
measureStart = measureAxis.getLocation(measureOffsetValue).round();
measureEnd =
measureAxis.getLocation(measureValue + measureOffsetValue).round();
}
Rectangle<int> bounds;
if (this.renderingVertically) {
// Rectangle clamps to zero width/height
bounds = new Rectangle<int>(domainStart, measureEnd,
domainEnd - domainStart, measureStart - measureEnd);
} else {
// Rectangle clamps to zero width/height
bounds = new Rectangle<int>(min(measureStart, measureEnd), domainStart,
(measureEnd - measureStart).abs(), domainEnd - domainStart);
}
return bounds;
}
@override
Rectangle<int> getBoundsForBar(BarRendererElement bar) => bar.bounds;
}
abstract class ImmutableBarRendererElement<D> {
ImmutableSeries<D> get series;
dynamic get datum;
int get index;
Rectangle<int> get bounds;
}
class BarRendererElement<D> extends BaseBarRendererElement
implements ImmutableBarRendererElement<D> {
ImmutableSeries<D> series;
Rectangle<int> bounds;
int roundPx;
int index;
dynamic _datum;
dynamic get datum => _datum;
set datum(dynamic datum) {
_datum = datum;
index = series?.data?.indexOf(datum);
}
BarRendererElement();
BarRendererElement.clone(BarRendererElement other) : super.clone(other) {
series = other.series;
bounds = other.bounds;
roundPx = other.roundPx;
index = other.index;
_datum = other._datum;
}
@override
void updateAnimationPercent(BaseBarRendererElement previous,
BaseBarRendererElement target, double animationPercent) {
final BarRendererElement localPrevious = previous;
final BarRendererElement localTarget = target;
final previousBounds = localPrevious.bounds;
final targetBounds = localTarget.bounds;
var top = ((targetBounds.top - previousBounds.top) * animationPercent) +
previousBounds.top;
var right =
((targetBounds.right - previousBounds.right) * animationPercent) +
previousBounds.right;
var bottom =
((targetBounds.bottom - previousBounds.bottom) * animationPercent) +
previousBounds.bottom;
var left = ((targetBounds.left - previousBounds.left) * animationPercent) +
previousBounds.left;
bounds = new Rectangle<int>(left.round(), top.round(),
(right - left).round(), (bottom - top).round());
roundPx = localTarget.roundPx;
super.updateAnimationPercent(previous, target, animationPercent);
}
}
class AnimatedBar<D> extends BaseAnimatedBar<D, BarRendererElement<D>> {
AnimatedBar(
{@required String key,
@required dynamic datum,
@required ImmutableSeries<D> series,
@required D domainValue})
: super(key: key, datum: datum, series: series, domainValue: domainValue);
@override
animateElementToMeasureAxisPosition(BaseBarRendererElement target) {
final BarRendererElement localTarget = target;
// TODO: Animate out bars in the middle of a stack.
localTarget.bounds = new Rectangle<int>(
localTarget.bounds.left + (localTarget.bounds.width / 2).round(),
localTarget.measureAxisPosition.round(),
0,
0);
}
BarRendererElement<D> getCurrentBar(double animationPercent) {
final BarRendererElement<D> bar = super.getCurrentBar(animationPercent);
// Update with series and datum information to pass to bar decorator.
bar.series = series;
bar.datum = datum;
return bar;
}
@override
BarRendererElement<D> clone(BarRendererElement bar) =>
new BarRendererElement<D>.clone(bar);
}

View File

@@ -0,0 +1,99 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../common/symbol_renderer.dart';
import '../common/chart_canvas.dart' show FillPatternType;
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
import 'bar_renderer.dart' show BarRenderer;
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
import 'base_bar_renderer_config.dart'
show BarGroupingType, BaseBarRendererConfig;
/// Configuration for a bar renderer.
class BarRendererConfig<D> extends BaseBarRendererConfig<D> {
/// Strategy for determining the corner radius of a bar.
final CornerStrategy cornerStrategy;
/// Decorator for optionally decorating painted bars.
final BarRendererDecorator barRendererDecorator;
BarRendererConfig({
String customRendererId,
CornerStrategy cornerStrategy,
FillPatternType fillPattern,
BarGroupingType groupingType,
int layoutPaintOrder = LayoutViewPaintOrder.bar,
int minBarLengthPx = 0,
double stackHorizontalSeparator,
double strokeWidthPx = 0.0,
this.barRendererDecorator,
SymbolRenderer symbolRenderer,
List<int> weightPattern,
}) : cornerStrategy = cornerStrategy ?? const ConstCornerStrategy(2),
super(
customRendererId: customRendererId,
groupingType: groupingType ?? BarGroupingType.grouped,
layoutPaintOrder: layoutPaintOrder,
minBarLengthPx: minBarLengthPx,
fillPattern: fillPattern,
stackHorizontalSeparator: stackHorizontalSeparator,
strokeWidthPx: strokeWidthPx,
symbolRenderer: symbolRenderer,
weightPattern: weightPattern,
);
@override
BarRenderer<D> build() {
return new BarRenderer<D>(config: this, rendererId: customRendererId);
}
@override
bool operator ==(other) {
if (identical(this, other)) {
return true;
}
if (!(other is BarRendererConfig)) {
return false;
}
return other.cornerStrategy == cornerStrategy && super == (other);
}
@override
int get hashCode {
var hash = super.hashCode;
hash = hash * 31 + (cornerStrategy?.hashCode ?? 0);
return hash;
}
}
abstract class CornerStrategy {
/// Returns the radius of the rounded corners in pixels.
int getRadius(int barWidth);
}
/// Strategy for constant corner radius.
class ConstCornerStrategy implements CornerStrategy {
final int radius;
const ConstCornerStrategy(this.radius);
@override
int getRadius(_) => radius;
}
/// Strategy for no corner radius.
class NoCornerStrategy extends ConstCornerStrategy {
const NoCornerStrategy() : super(0);
}

View File

@@ -0,0 +1,34 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:meta/meta.dart' show required;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../common/chart_canvas.dart' show ChartCanvas;
import 'bar_renderer.dart' show ImmutableBarRendererElement;
/// Decorates bars after the bars have already been painted.
abstract class BarRendererDecorator<D> {
const BarRendererDecorator();
void decorate(Iterable<ImmutableBarRendererElement<D>> barElements,
ChartCanvas canvas, GraphicsFactory graphicsFactory,
{@required Rectangle drawBounds,
@required double animationPercent,
@required bool renderingVertically,
bool rtl = false});
}

View File

@@ -0,0 +1,422 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point, Rectangle, max, min;
import 'package:meta/meta.dart' show required;
import '../../common/color.dart' show Color;
import '../cartesian/axis/axis.dart'
show ImmutableAxis, domainAxisKey, measureAxisKey;
import '../common/chart_canvas.dart' show ChartCanvas, FillPatternType;
import '../common/datum_details.dart' show DatumDetails;
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
import '../common/series_datum.dart' show SeriesDatum;
import 'bar_target_line_renderer_config.dart' show BarTargetLineRendererConfig;
import 'base_bar_renderer.dart'
show
BaseBarRenderer,
barGroupCountKey,
barGroupIndexKey,
previousBarGroupWeightKey,
barGroupWeightKey;
import 'base_bar_renderer_element.dart'
show BaseAnimatedBar, BaseBarRendererElement;
/// Renders series data as a series of bar target lines.
///
/// Usually paired with a BarRenderer to display target metrics alongside actual
/// metrics.
class BarTargetLineRenderer<D> extends BaseBarRenderer<D,
_BarTargetLineRendererElement, _AnimatedBarTargetLine<D>> {
/// If we are grouped, use this spacing between the bars in a group.
final _barGroupInnerPadding = 2;
/// Standard color for all bar target lines.
final _color = new Color(r: 0, g: 0, b: 0, a: 153);
factory BarTargetLineRenderer(
{BarTargetLineRendererConfig<D> config,
String rendererId = 'barTargetLine'}) {
config ??= new BarTargetLineRendererConfig<D>();
return new BarTargetLineRenderer._internal(
config: config, rendererId: rendererId);
}
BarTargetLineRenderer._internal(
{BarTargetLineRendererConfig<D> config, String rendererId})
: super(
config: config,
rendererId: rendererId,
layoutPaintOrder: config.layoutPaintOrder);
@override
void configureSeries(List<MutableSeries<D>> seriesList) {
seriesList.forEach((MutableSeries<D> series) {
series.colorFn ??= (_) => _color;
series.fillColorFn ??= (_) => _color;
});
}
DatumDetails<D> addPositionToDetailsForSeriesDatum(
DatumDetails<D> details, SeriesDatum<D> seriesDatum) {
final series = details.series;
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final barGroupIndex = series.getAttr(barGroupIndexKey);
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
final barGroupWeight = series.getAttr(barGroupWeightKey);
final numBarGroups = series.getAttr(barGroupCountKey);
final points = _getTargetLinePoints(
details.domain,
domainAxis,
domainAxis.rangeBand.round(),
details.measure,
details.measureOffset,
measureAxis,
barGroupIndex,
previousBarGroupWeight,
barGroupWeight,
numBarGroups);
Point<double> chartPosition;
if (renderingVertically) {
chartPosition = new Point<double>(
(points[0].x + (points[1].x - points[0].x) / 2).toDouble(),
points[0].y.toDouble());
} else {
chartPosition = new Point<double>(points[0].x.toDouble(),
(points[0].y + (points[1].y - points[0].y) / 2).toDouble());
}
return new DatumDetails.from(details, chartPosition: chartPosition);
}
@override
_BarTargetLineRendererElement getBaseDetails(dynamic datum, int index) {
final BarTargetLineRendererConfig<D> localConfig = config;
return new _BarTargetLineRendererElement()
..roundEndCaps = localConfig.roundEndCaps;
}
/// Generates an [_AnimatedBarTargetLine] to represent the previous and
/// current state of one bar target line on the chart.
@override
_AnimatedBarTargetLine<D> makeAnimatedBar(
{String key,
ImmutableSeries<D> series,
dynamic datum,
Color color,
List<int> dashPattern,
_BarTargetLineRendererElement details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
Color fillColor,
FillPatternType fillPattern,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups,
double strokeWidthPx,
bool measureIsNull,
bool measureIsNegative}) {
return new _AnimatedBarTargetLine(
key: key, datum: datum, series: series, domainValue: domainValue)
..setNewTarget(makeBarRendererElement(
color: color,
details: details,
dashPattern: dashPattern,
domainValue: domainValue,
domainAxis: domainAxis,
domainWidth: domainWidth,
measureValue: measureValue,
measureOffsetValue: measureOffsetValue,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
fillColor: fillColor,
fillPattern: fillPattern,
strokeWidthPx: strokeWidthPx,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
numBarGroups: numBarGroups,
measureIsNull: measureIsNull,
measureIsNegative: measureIsNegative));
}
/// Generates a [_BarTargetLineRendererElement] to represent the rendering
/// data for one bar target line on the chart.
@override
_BarTargetLineRendererElement makeBarRendererElement(
{Color color,
List<int> dashPattern,
_BarTargetLineRendererElement details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups,
bool measureIsNull,
bool measureIsNegative}) {
return new _BarTargetLineRendererElement()
..color = color
..dashPattern = dashPattern
..fillColor = fillColor
..fillPattern = fillPattern
..measureAxisPosition = measureAxisPosition
..roundEndCaps = details.roundEndCaps
..strokeWidthPx = strokeWidthPx
..measureIsNull = measureIsNull
..measureIsNegative = measureIsNegative
..points = _getTargetLinePoints(
domainValue,
domainAxis,
domainWidth,
measureValue,
measureOffsetValue,
measureAxis,
barGroupIndex,
previousBarGroupWeight,
barGroupWeight,
numBarGroups);
}
@override
void paintBar(
ChartCanvas canvas,
double animationPercent,
Iterable<_BarTargetLineRendererElement> barElements,
) {
barElements.forEach((_BarTargetLineRendererElement bar) {
// TODO: Combine common line attributes into
// GraphicsFactory.lineStyle or similar.
canvas.drawLine(
clipBounds: drawBounds,
points: bar.points,
stroke: bar.color,
roundEndCaps: bar.roundEndCaps,
strokeWidthPx: bar.strokeWidthPx);
});
}
/// Generates a set of points that describe a bar target line.
List<Point<int>> _getTargetLinePoints(
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
int numBarGroups) {
// If no weights were passed in, default to equal weight per bar.
if (barGroupWeight == null) {
barGroupWeight = 1 / numBarGroups;
previousBarGroupWeight = barGroupIndex * barGroupWeight;
}
final BarTargetLineRendererConfig<D> localConfig = config;
// Calculate how wide each bar target line should be within the group of
// bar target lines. If we only have one series, or are stacked, then
// barWidth should equal domainWidth.
int spacingLoss = (_barGroupInnerPadding * (numBarGroups - 1));
int barWidth = ((domainWidth - spacingLoss) * barGroupWeight).round();
// Get the overdraw boundaries.
var overDrawOuterPx = localConfig.overDrawOuterPx;
var overDrawPx = localConfig.overDrawPx;
int overDrawStartPx = (barGroupIndex == 0) && overDrawOuterPx != null
? overDrawOuterPx
: overDrawPx;
int overDrawEndPx =
(barGroupIndex == numBarGroups - 1) && overDrawOuterPx != null
? overDrawOuterPx
: overDrawPx;
// Flip bar group index for calculating location on the domain axis if RTL.
final adjustedBarGroupIndex =
isRtl ? numBarGroups - barGroupIndex - 1 : barGroupIndex;
// Calculate the start and end of the bar target line, taking into account
// accumulated padding for grouped bars.
num previousAverageWidth = adjustedBarGroupIndex > 0
? ((domainWidth - spacingLoss) *
(previousBarGroupWeight / adjustedBarGroupIndex))
.round()
: 0;
int domainStart = (domainAxis.getLocation(domainValue) -
(domainWidth / 2) +
(previousAverageWidth + _barGroupInnerPadding) *
adjustedBarGroupIndex -
overDrawStartPx)
.round();
int domainEnd = domainStart + barWidth + overDrawStartPx + overDrawEndPx;
measureValue = measureValue != null ? measureValue : 0;
// Calculate measure locations. Stacked bars should have their
// offset calculated previously.
int measureStart =
measureAxis.getLocation(measureValue + measureOffsetValue).round();
List<Point<int>> points;
if (renderingVertically) {
points = [
new Point<int>(domainStart, measureStart),
new Point<int>(domainEnd, measureStart)
];
} else {
points = [
new Point<int>(measureStart, domainStart),
new Point<int>(measureStart, domainEnd)
];
}
return points;
}
@override
Rectangle<int> getBoundsForBar(_BarTargetLineRendererElement bar) {
final points = bar.points;
int top;
int bottom;
int left;
int right;
points.forEach((Point<int> p) {
top = top != null ? min(top, p.y) : p.y;
left = left != null ? min(left, p.x) : p.x;
bottom = bottom != null ? max(bottom, p.y) : p.y;
right = right != null ? max(right, p.x) : p.x;
});
return new Rectangle<int>(left, top, right - left, bottom - top);
}
}
class _BarTargetLineRendererElement extends BaseBarRendererElement {
List<Point<int>> points;
bool roundEndCaps;
_BarTargetLineRendererElement();
_BarTargetLineRendererElement.clone(_BarTargetLineRendererElement other)
: super.clone(other) {
points = new List<Point<int>>.from(other.points);
roundEndCaps = other.roundEndCaps;
}
@override
void updateAnimationPercent(BaseBarRendererElement previous,
BaseBarRendererElement target, double animationPercent) {
final _BarTargetLineRendererElement localPrevious = previous;
final _BarTargetLineRendererElement localTarget = target;
final previousPoints = localPrevious.points;
final targetPoints = localTarget.points;
Point<int> lastPoint;
int pointIndex;
for (pointIndex = 0; pointIndex < targetPoints.length; pointIndex++) {
var targetPoint = targetPoints[pointIndex];
// If we have more points than the previous line, animate in the new point
// by starting its measure position at the last known official point.
Point<int> previousPoint;
if (previousPoints.length - 1 >= pointIndex) {
previousPoint = previousPoints[pointIndex];
lastPoint = previousPoint;
} else {
previousPoint = new Point<int>(targetPoint.x, lastPoint.y);
}
var x = ((targetPoint.x - previousPoint.x) * animationPercent) +
previousPoint.x;
var y = ((targetPoint.y - previousPoint.y) * animationPercent) +
previousPoint.y;
if (points.length - 1 >= pointIndex) {
points[pointIndex] = new Point<int>(x.round(), y.round());
} else {
points.add(new Point<int>(x.round(), y.round()));
}
}
// Removing extra points that don't exist anymore.
if (pointIndex < points.length) {
points.removeRange(pointIndex, points.length);
}
strokeWidthPx = ((localTarget.strokeWidthPx - localPrevious.strokeWidthPx) *
animationPercent) +
localPrevious.strokeWidthPx;
roundEndCaps = localTarget.roundEndCaps;
super.updateAnimationPercent(previous, target, animationPercent);
}
}
class _AnimatedBarTargetLine<D>
extends BaseAnimatedBar<D, _BarTargetLineRendererElement> {
_AnimatedBarTargetLine(
{@required String key,
@required dynamic datum,
@required ImmutableSeries<D> series,
@required D domainValue})
: super(key: key, datum: datum, series: series, domainValue: domainValue);
@override
animateElementToMeasureAxisPosition(BaseBarRendererElement target) {
final _BarTargetLineRendererElement localTarget = target;
final newPoints = <Point<int>>[];
for (var index = 0; index < localTarget.points.length; index++) {
final targetPoint = localTarget.points[index];
newPoints.add(new Point<int>(
targetPoint.x, localTarget.measureAxisPosition.round()));
}
localTarget.points = newPoints;
}
@override
_BarTargetLineRendererElement clone(_BarTargetLineRendererElement bar) =>
new _BarTargetLineRendererElement.clone(bar);
}

View File

@@ -0,0 +1,92 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../common/symbol_renderer.dart'
show SymbolRenderer, LineSymbolRenderer;
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
import 'bar_target_line_renderer.dart' show BarTargetLineRenderer;
import 'base_bar_renderer_config.dart'
show BarGroupingType, BaseBarRendererConfig;
/// Configuration for a bar target line renderer.
class BarTargetLineRendererConfig<D> extends BaseBarRendererConfig<D> {
/// The number of pixels that the line will extend beyond the bandwidth at the
/// edges of the bar group.
///
/// If set, this overrides overDrawPx for the beginning side of the first bar
/// target line in the group, and the ending side of the last bar target line.
/// overDrawPx will be used for overdrawing the target lines for interior
/// sides of the bars.
final int overDrawOuterPx;
/// The number of pixels that the line will extend beyond the bandwidth for
/// every bar in a group.
final int overDrawPx;
/// Whether target lines should have round end caps, or square if false.
final bool roundEndCaps;
BarTargetLineRendererConfig(
{String customRendererId,
List<int> dashPattern,
groupingType = BarGroupingType.grouped,
int layoutPaintOrder = LayoutViewPaintOrder.barTargetLine,
int minBarLengthPx = 0,
this.overDrawOuterPx,
this.overDrawPx = 0,
this.roundEndCaps = true,
double strokeWidthPx = 3.0,
SymbolRenderer symbolRenderer,
List<int> weightPattern})
: super(
customRendererId: customRendererId,
dashPattern: dashPattern,
groupingType: groupingType,
layoutPaintOrder: layoutPaintOrder,
minBarLengthPx: minBarLengthPx,
strokeWidthPx: strokeWidthPx,
symbolRenderer: symbolRenderer ?? new LineSymbolRenderer(),
weightPattern: weightPattern,
);
@override
BarTargetLineRenderer<D> build() {
return new BarTargetLineRenderer<D>(
config: this, rendererId: customRendererId);
}
@override
bool operator ==(other) {
if (identical(this, other)) {
return true;
}
if (!(other is BarTargetLineRendererConfig)) {
return false;
}
return other.overDrawOuterPx == overDrawOuterPx &&
other.overDrawPx == overDrawPx &&
other.roundEndCaps == roundEndCaps &&
super == (other);
}
@override
int get hashCode {
var hash = 1;
hash = hash * 31 + (overDrawOuterPx?.hashCode ?? 0);
hash = hash * 31 + (overDrawPx?.hashCode ?? 0);
hash = hash * 31 + (roundEndCaps?.hashCode ?? 0);
return hash;
}
}

View File

@@ -0,0 +1,803 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap, HashSet;
import 'dart:math' show Point, Rectangle, max;
import 'package:meta/meta.dart' show protected, required;
import '../../common/color.dart' show Color;
import '../../common/math.dart' show clamp;
import '../../common/symbol_renderer.dart' show RoundedRectSymbolRenderer;
import '../../data/series.dart' show AttributeKey;
import '../cartesian/axis/axis.dart'
show ImmutableAxis, OrdinalAxis, domainAxisKey, measureAxisKey;
import '../cartesian/axis/scale.dart' show RangeBandConfig;
import '../cartesian/cartesian_renderer.dart' show BaseCartesianRenderer;
import '../common/base_chart.dart' show BaseChart;
import '../common/chart_canvas.dart' show ChartCanvas, FillPatternType;
import '../common/datum_details.dart' show DatumDetails;
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
import 'base_bar_renderer_config.dart' show BaseBarRendererConfig;
import 'base_bar_renderer_element.dart'
show BaseAnimatedBar, BaseBarRendererElement;
const barGroupIndexKey = const AttributeKey<int>('BarRenderer.barGroupIndex');
const barGroupCountKey = const AttributeKey<int>('BarRenderer.barGroupCount');
const barGroupWeightKey =
const AttributeKey<double>('BarRenderer.barGroupWeight');
const previousBarGroupWeightKey =
const AttributeKey<double>('BarRenderer.previousBarGroupWeight');
const stackKeyKey = const AttributeKey<String>('BarRenderer.stackKey');
const barElementsKey =
const AttributeKey<List<BaseBarRendererElement>>('BarRenderer.elements');
/// Base class for bar renderers that implements common stacking and grouping
/// logic.
///
/// Bar renderers support 4 different modes of rendering multiple series on the
/// chart, configured by the grouped and stacked flags.
/// * grouped - Render bars for each series that shares a domain value
/// side-by-side.
/// * stacked - Render bars for each series that shares a domain value in a
/// stack, ordered in the same order as the series list.
/// * grouped-stacked: Render bars for each series that shares a domain value in
/// a group of bar stacks. Each stack will contain all the series that share a
/// series category.
/// * floating style - When grouped and stacked are both false, all bars that
/// share a domain value will be rendered in the same domain space. Each datum
/// should be configured with a measure offset to position its bar along the
/// measure axis. Bars will freely overlap if their measure values and measure
/// offsets overlap. Note that bars for each series will be rendered in order,
/// such that bars from the last series will be "on top" of bars from previous
/// series.
abstract class BaseBarRenderer<D, R extends BaseBarRendererElement,
B extends BaseAnimatedBar<D, R>> extends BaseCartesianRenderer<D> {
final BaseBarRendererConfig config;
@protected
BaseChart<D> chart;
/// Store a map of domain+barGroupIndex+category index to bars in a stack.
///
/// This map is used to render all the bars in a stack together, to account
/// for rendering effects that need to take the full stack into account (e.g.
/// corner rounding).
///
/// [LinkedHashMap] is used to render the bars on the canvas in the same order
/// as the data was given to the chart. For the case where both grouping and
/// stacking are disabled, this means that bars for data later in the series
/// will be drawn "on top of" bars earlier in the series.
final _barStackMap = new LinkedHashMap<String, List<B>>();
// Store a list of bar stacks that exist in the series data.
//
// This list will be used to remove any AnimatingBars that were rendered in
// previous draw cycles, but no longer have a corresponding datum in the new
// data.
final _currentKeys = <String>[];
/// Stores a list of stack keys for each group key.
final _currentGroupsStackKeys = new LinkedHashMap<D, Set<String>>();
/// Optimization for getNearest to avoid scanning all data if possible.
ImmutableAxis<D> _prevDomainAxis;
BaseBarRenderer(
{@required this.config, String rendererId, int layoutPaintOrder})
: super(
rendererId: rendererId,
layoutPaintOrder: layoutPaintOrder,
symbolRenderer:
config?.symbolRenderer ?? new RoundedRectSymbolRenderer(),
);
@override
void preprocessSeries(List<MutableSeries<D>> seriesList) {
var barGroupIndex = 0;
// Maps used to store the final measure offset of the previous series, for
// each domain value.
final posDomainToStackKeyToDetailsMap = {};
final negDomainToStackKeyToDetailsMap = {};
final categoryToIndexMap = {};
// Keep track of the largest bar stack size. This should be 1 for grouped
// bars, and it should be the size of the tallest stack for stacked or
// grouped stacked bars.
var maxBarStackSize = 0;
final orderedSeriesList = getOrderedSeriesList(seriesList);
orderedSeriesList.forEach((MutableSeries<D> series) {
var elements = <BaseBarRendererElement>[];
var domainFn = series.domainFn;
var measureFn = series.measureFn;
var measureOffsetFn = series.measureOffsetFn;
var fillPatternFn = series.fillPatternFn;
var strokeWidthPxFn = series.strokeWidthPxFn;
series.dashPatternFn ??= (_) => config.dashPattern;
// Identifies which stack the series will go in, by default a single
// stack.
var stackKey = '__defaultKey__';
// Override the stackKey with seriesCategory if we are GROUPED_STACKED
// so we have a way to choose which series go into which stacks.
if (config.grouped && config.stacked) {
if (series.seriesCategory != null) {
stackKey = series.seriesCategory;
}
barGroupIndex = categoryToIndexMap[stackKey];
if (barGroupIndex == null) {
barGroupIndex = categoryToIndexMap.length;
categoryToIndexMap[stackKey] = barGroupIndex;
}
}
var needsMeasureOffset = false;
for (var barIndex = 0; barIndex < series.data.length; barIndex++) {
dynamic datum = series.data[barIndex];
final details = getBaseDetails(datum, barIndex);
details.barStackIndex = 0;
details.measureOffset = 0;
if (fillPatternFn != null) {
details.fillPattern = fillPatternFn(barIndex);
} else {
details.fillPattern = config.fillPattern;
}
if (strokeWidthPxFn != null) {
details.strokeWidthPx = strokeWidthPxFn(barIndex).toDouble();
} else {
details.strokeWidthPx = config.strokeWidthPx;
}
// When stacking is enabled, adjust the measure offset for each domain
// value in each series by adding up the measures and offsets of lower
// series.
if (config.stacked) {
needsMeasureOffset = true;
var domain = domainFn(barIndex);
var measure = measureFn(barIndex);
// We will render positive bars in one stack, and negative bars in a
// separate stack. Keep track of the measure offsets for these stacks
// independently.
var domainToCategoryToDetailsMap = measure == null || measure >= 0
? posDomainToStackKeyToDetailsMap
: negDomainToStackKeyToDetailsMap;
var categoryToDetailsMap =
domainToCategoryToDetailsMap.putIfAbsent(domain, () => {});
var prevDetail = categoryToDetailsMap[stackKey];
if (prevDetail != null) {
details.barStackIndex = prevDetail.barStackIndex + 1;
}
details.cumulativeTotal = measure != null ? measure : 0;
// Get the previous series' measure offset.
var measureOffset = measureOffsetFn(barIndex);
if (prevDetail != null) {
measureOffset += prevDetail.measureOffsetPlusMeasure;
details.cumulativeTotal += prevDetail.cumulativeTotal;
}
// And overwrite the details measure offset.
details.measureOffset = measureOffset;
var measureValue = (measure != null ? measure : 0);
details.measureOffsetPlusMeasure = measureOffset + measureValue;
categoryToDetailsMap[stackKey] = details;
}
maxBarStackSize = max(maxBarStackSize, details.barStackIndex + 1);
elements.add(details);
}
if (needsMeasureOffset) {
// Override the measure offset function to return the measure offset we
// calculated for each datum. This already includes any measure offset
// that was configured in the series data.
series.measureOffsetFn = (index) => elements[index].measureOffset;
}
series.setAttr(barGroupIndexKey, barGroupIndex);
series.setAttr(stackKeyKey, stackKey);
series.setAttr(barElementsKey, elements);
if (config.grouped) {
barGroupIndex++;
}
});
// Compute number of bar groups. This must be done after we have processed
// all of the series once, so that we know how many categories we have.
var numBarGroups = 0;
if (config.grouped && config.stacked) {
// For grouped stacked bars, categoryToIndexMap effectively one list per
// group of stacked bars.
numBarGroups = categoryToIndexMap.length;
} else if (config.stacked) {
numBarGroups = 1;
} else {
numBarGroups = seriesList.length;
}
// Compute bar group weights.
final barWeights = _calculateBarWeights(numBarGroups);
seriesList.forEach((MutableSeries<D> series) {
series.setAttr(barGroupCountKey, numBarGroups);
if (barWeights.isNotEmpty) {
final barGroupIndex = series.getAttr(barGroupIndexKey);
final barWeight = barWeights[barGroupIndex];
// In RTL mode, we need to grab the weights for the bars that follow
// this datum in the series (instead of precede it). The first datum is
// physically positioned on the canvas to the right of all the rest of
// the bar group data that follows it.
final previousBarWeights = isRtl
? barWeights.getRange(barGroupIndex + 1, numBarGroups)
: barWeights.getRange(0, barGroupIndex);
final previousBarWeight = previousBarWeights.isNotEmpty
? previousBarWeights.reduce((a, b) => a + b)
: 0.0;
series.setAttr(barGroupWeightKey, barWeight);
series.setAttr(previousBarGroupWeightKey, previousBarWeight);
}
});
}
/// Calculates bar weights for a list of series from [config.weightPattern].
///
/// If [config.weightPattern] is not set, then this will assign a weight
/// proportional to the number of bar groups for every series.
List<double> _calculateBarWeights(int numBarGroups) {
// Set up bar weights for each series as a ratio of the total weight.
final weights = <double>[];
if (config.weightPattern != null) {
if (numBarGroups > config.weightPattern.length) {
throw new ArgumentError('Number of series exceeds length of weight '
'pattern ${config.weightPattern}');
}
var totalBarWeight = 0;
for (var i = 0; i < numBarGroups; i++) {
totalBarWeight += config.weightPattern[i];
}
for (var i = 0; i < numBarGroups; i++) {
weights.add(config.weightPattern[i] / totalBarWeight);
}
} else {
for (var i = 0; i < numBarGroups; i++) {
weights.add(1 / numBarGroups);
}
}
return weights;
}
/// Construct a base details element for a given datum.
///
/// This is intended to be overridden by child classes that need to add
/// customized rendering properties.
R getBaseDetails(dynamic datum, int index);
@override
void configureDomainAxes(List<MutableSeries<D>> seriesList) {
super.configureDomainAxes(seriesList);
// Configure the domain axis to use a range band configuration.
if (seriesList.isNotEmpty) {
// Given that charts can only have one domain axis, just grab it from the
// first series.
final domainAxis = seriesList.first.getAttr(domainAxisKey);
domainAxis.setRangeBandConfig(new RangeBandConfig.styleAssignedPercent());
}
}
void update(List<ImmutableSeries<D>> seriesList, bool isAnimatingThisDraw) {
_currentKeys.clear();
_currentGroupsStackKeys.clear();
final orderedSeriesList = getOrderedSeriesList(seriesList);
orderedSeriesList.forEach((final ImmutableSeries<D> series) {
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final domainFn = series.domainFn;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final measureFn = series.measureFn;
final colorFn = series.colorFn;
final dashPatternFn = series.dashPatternFn;
final fillColorFn = series.fillColorFn;
final seriesStackKey = series.getAttr(stackKeyKey);
final barGroupCount = series.getAttr(barGroupCountKey);
final barGroupIndex = series.getAttr(barGroupIndexKey);
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
final barGroupWeight = series.getAttr(barGroupWeightKey);
final measureAxisPosition = measureAxis.getLocation(0.0);
var elementsList = series.getAttr(barElementsKey);
// Save off domainAxis for getNearest.
_prevDomainAxis = domainAxis;
for (var barIndex = 0; barIndex < series.data.length; barIndex++) {
final datum = series.data[barIndex];
BaseBarRendererElement details = elementsList[barIndex];
D domainValue = domainFn(barIndex);
final measureValue = measureFn(barIndex);
final measureIsNull = measureValue == null;
final measureIsNegative = !measureIsNull && measureValue < 0;
// Each bar should be stored in barStackMap in a structure that mirrors
// the visual rendering of the bars. Thus, they should be grouped by
// domain value, series category (by way of the stack keys that were
// generated for each series in the preprocess step), and bar group
// index to account for all combinations of grouping and stacking.
var barStackMapKey = domainValue.toString() +
'__' +
seriesStackKey +
'__' +
(measureIsNegative ? 'pos' : 'neg') +
'__' +
barGroupIndex.toString();
var barKey = barStackMapKey + details.barStackIndex.toString();
var barStackList = _barStackMap.putIfAbsent(barStackMapKey, () => []);
// If we already have an AnimatingBarfor that index, use it.
var animatingBar = barStackList.firstWhere((B bar) => bar.key == barKey,
orElse: () => null);
// If we don't have any existing bar element, create a new bar and have
// it animate in from the domain axis.
// TODO: Animate bars in the middle of a stack from their
// nearest neighbors, instead of the measure axis.
if (animatingBar == null) {
// If the measure is null and there was no existing animating bar, it
// means we don't need to draw this bar at all.
if (!measureIsNull) {
animatingBar = makeAnimatedBar(
key: barKey,
series: series,
datum: datum,
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: colorFn(barIndex),
dashPattern: dashPatternFn(barIndex),
details: details,
domainValue: domainFn(barIndex),
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: fillColorFn(barIndex),
fillPattern: details.fillPattern,
measureValue: 0.0,
measureOffsetValue: 0.0,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: details.strokeWidthPx,
measureIsNull: measureIsNull,
measureIsNegative: measureIsNegative);
barStackList.add(animatingBar);
}
} else {
animatingBar
..datum = datum
..series = series
..domainValue = domainValue;
}
if (animatingBar == null) {
continue;
}
// Update the set of bars that still exist in the series data.
_currentKeys.add(barKey);
// Store off stack keys for each bar group to help getNearest identify
// groups of stacks.
_currentGroupsStackKeys
.putIfAbsent(domainValue, () => new Set<String>())
.add(barStackMapKey);
// Get the barElement we are going to setup.
// Optimization to prevent allocation in non-animating case.
BaseBarRendererElement barElement = makeBarRendererElement(
barGroupIndex: barGroupIndex,
previousBarGroupWeight: previousBarGroupWeight,
barGroupWeight: barGroupWeight,
color: colorFn(barIndex),
dashPattern: dashPatternFn(barIndex),
details: details,
domainValue: domainFn(barIndex),
domainAxis: domainAxis,
domainWidth: domainAxis.rangeBand.round(),
fillColor: fillColorFn(barIndex),
fillPattern: details.fillPattern,
measureValue: measureValue,
measureOffsetValue: details.measureOffset,
measureAxisPosition: measureAxisPosition,
measureAxis: measureAxis,
numBarGroups: barGroupCount,
strokeWidthPx: details.strokeWidthPx,
measureIsNull: measureIsNull,
measureIsNegative: measureIsNegative);
animatingBar.setNewTarget(barElement);
}
});
// Animate out bars that don't exist anymore.
_barStackMap.forEach((String key, List<B> barStackList) {
for (var barIndex = 0; barIndex < barStackList.length; barIndex++) {
final bar = barStackList[barIndex];
if (_currentKeys.contains(bar.key) != true) {
bar.animateOut();
}
}
});
}
/// Generates a [BaseAnimatedBar] to represent the previous and current state
/// of one bar on the chart.
B makeAnimatedBar(
{String key,
ImmutableSeries<D> series,
dynamic datum,
int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
Color color,
List<int> dashPattern,
R details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
int numBarGroups,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
bool measureIsNull,
bool measureIsNegative});
/// Generates a [BaseBarRendererElement] to represent the rendering data for
/// one bar on the chart.
R makeBarRendererElement(
{int barGroupIndex,
double previousBarGroupWeight,
double barGroupWeight,
Color color,
List<int> dashPattern,
R details,
D domainValue,
ImmutableAxis<D> domainAxis,
int domainWidth,
num measureValue,
num measureOffsetValue,
ImmutableAxis<num> measureAxis,
double measureAxisPosition,
int numBarGroups,
Color fillColor,
FillPatternType fillPattern,
double strokeWidthPx,
bool measureIsNull,
bool measureIsNegative});
@override
void onAttach(BaseChart<D> chart) {
super.onAttach(chart);
// We only need the chart.context.isRtl setting, but context is not yet
// available when the default renderer is attached to the chart on chart
// creation time, since chart onInit is called after the chart is created.
this.chart = chart;
}
/// Paints the current bar data on the canvas.
void paint(ChartCanvas canvas, double animationPercent) {
// Clean up the bars that no longer exist.
if (animationPercent == 1.0) {
final keysToRemove = new HashSet<String>();
_barStackMap.forEach((String key, List<B> barStackList) {
barStackList.retainWhere(
(B bar) => !bar.animatingOut && !bar.targetBar.measureIsNull);
if (barStackList.isEmpty) {
keysToRemove.add(key);
}
});
// When cleaning up the animation, also clean up the keys used to lookup
// if a bar is selected.
for (String key in keysToRemove) {
_barStackMap.remove(key);
_currentKeys.remove(key);
}
_currentGroupsStackKeys.forEach((domain, keys) {
keys.removeWhere(keysToRemove.contains);
});
}
_barStackMap.forEach((String stackKey, List<B> barStack) {
// Turn this into a list so that the getCurrentBar isn't called more than
// once for each animationPercent if the barElements are iterated more
// than once.
final barElements = barStack
.map((B animatingBar) => animatingBar.getCurrentBar(animationPercent))
.toList();
if (barElements.isNotEmpty) {
paintBar(canvas, animationPercent, barElements);
}
});
}
/// Paints a stack of bar elements on the canvas.
void paintBar(
ChartCanvas canvas, double animationPercent, Iterable<R> barElements);
@override
List<DatumDetails<D>> getNearestDatumDetailPerSeries(
Point<double> chartPoint, bool byDomain, Rectangle<int> boundsOverride) {
var nearest = <DatumDetails<D>>[];
// Was it even in the component bounds?
if (!isPointWithinBounds(chartPoint, boundsOverride)) {
return nearest;
}
if (_prevDomainAxis is OrdinalAxis) {
final domainValue = _prevDomainAxis
.getDomain(renderingVertically ? chartPoint.x : chartPoint.y);
// If we have a domainValue for the event point, then find all segments
// that match it.
if (domainValue != null) {
if (renderingVertically) {
nearest = _getVerticalDetailsForDomainValue(domainValue, chartPoint);
} else {
nearest =
_getHorizontalDetailsForDomainValue(domainValue, chartPoint);
}
}
} else {
if (renderingVertically) {
nearest = _getVerticalDetailsForDomainValue(null, chartPoint);
} else {
nearest = _getHorizontalDetailsForDomainValue(null, chartPoint);
}
// Find the closest domain and only keep values that match the domain.
var minRelativeDistance = double.maxFinite;
var minDomainDistance = double.maxFinite;
var minMeasureDistance = double.maxFinite;
D nearestDomain;
// TODO: Optimize this with a binary search based on chartX.
for (DatumDetails<D> detail in nearest) {
if (byDomain) {
if (detail.domainDistance < minDomainDistance ||
(detail.domainDistance == minDomainDistance &&
detail.measureDistance < minMeasureDistance)) {
minDomainDistance = detail.domainDistance;
minMeasureDistance = detail.measureDistance;
nearestDomain = detail.domain;
}
} else {
if (detail.relativeDistance < minRelativeDistance) {
minRelativeDistance = detail.relativeDistance;
nearestDomain = detail.domain;
}
}
}
nearest.retainWhere((d) => d.domain == nearestDomain);
}
// If we didn't find anything, then keep an empty list.
nearest ??= <DatumDetails<D>>[];
// Note: the details are already sorted by domain & measure distance in
// base chart.
return nearest;
}
Rectangle<int> getBoundsForBar(R bar);
@protected
List<BaseAnimatedBar<D, R>> _getSegmentsForDomainValue(D domainValue,
{bool where(BaseAnimatedBar<D, R> bar)}) {
final matchingSegments = <BaseAnimatedBar<D, R>>[];
// [domainValue] is null only when the bar renderer is being used with in
// a non ordinal axis (ex. date time axis).
//
// In the case of null [domainValue] return all values to be compared, since
// we can't use the optimized comparison for [OrdinalAxis].
final stackKeys = (domainValue != null)
? _currentGroupsStackKeys[domainValue]
: _currentGroupsStackKeys.values
.reduce((allKeys, keys) => allKeys..addAll(keys));
stackKeys?.forEach((String stackKey) {
if (where != null) {
matchingSegments.addAll(_barStackMap[stackKey].where(where));
} else {
matchingSegments.addAll(_barStackMap[stackKey]);
}
});
return matchingSegments;
}
// In the case of null [domainValue] return all values to be compared, since
// we can't use the optimized comparison for [OrdinalAxis].
List<DatumDetails<D>> _getVerticalDetailsForDomainValue(
D domainValue, Point<double> chartPoint) {
return new List<DatumDetails<D>>.from(_getSegmentsForDomainValue(
domainValue,
where: (BaseAnimatedBar<D, R> bar) => !bar.series.overlaySeries)
.map<DatumDetails<D>>((BaseAnimatedBar<D, R> bar) {
final barBounds = getBoundsForBar(bar.currentBar);
final segmentDomainDistance =
_getDistance(chartPoint.x.round(), barBounds.left, barBounds.right);
final segmentMeasureDistance =
_getDistance(chartPoint.y.round(), barBounds.top, barBounds.bottom);
final nearestPoint = new Point<double>(
clamp(chartPoint.x, barBounds.left, barBounds.right).toDouble(),
clamp(chartPoint.y, barBounds.top, barBounds.bottom).toDouble());
final relativeDistance = chartPoint.distanceTo(nearestPoint);
return new DatumDetails<D>(
series: bar.series,
datum: bar.datum,
domain: bar.domainValue,
domainDistance: segmentDomainDistance,
measureDistance: segmentMeasureDistance,
relativeDistance: relativeDistance,
);
}));
}
List<DatumDetails<D>> _getHorizontalDetailsForDomainValue(
D domainValue, Point<double> chartPoint) {
return new List<DatumDetails<D>>.from(_getSegmentsForDomainValue(
domainValue,
where: (BaseAnimatedBar<D, R> bar) => !bar.series.overlaySeries)
.map((BaseAnimatedBar<D, R> bar) {
final barBounds = getBoundsForBar(bar.currentBar);
final segmentDomainDistance =
_getDistance(chartPoint.y.round(), barBounds.top, barBounds.bottom);
final segmentMeasureDistance =
_getDistance(chartPoint.x.round(), barBounds.left, barBounds.right);
return new DatumDetails<D>(
series: bar.series,
datum: bar.datum,
domain: bar.domainValue,
domainDistance: segmentDomainDistance,
measureDistance: segmentMeasureDistance,
);
}));
}
double _getDistance(int point, int min, int max) {
if (max >= point && min <= point) {
return 0.0;
}
return (point > max ? (point - max) : (min - point)).toDouble();
}
/// Gets the iterator for the series based grouped/stacked and orientation.
///
/// For vertical stacked bars:
/// * If grouped, return the iterator that keeps the category order but
/// reverse the order of the series so the first series is on the top of the
/// stack.
/// * Otherwise, return iterator of the reversed list
///
/// All other types, use the in order iterator.
@protected
Iterable<S> getOrderedSeriesList<S extends ImmutableSeries>(
List<S> seriesList) {
return (renderingVertically && config.stacked)
? config.grouped
? new _ReversedSeriesIterable(seriesList)
: seriesList.reversed
: seriesList;
}
bool get isRtl => chart.context.isRtl;
}
/// Iterable wrapping the seriesList that returns the ReversedSeriesItertor.
class _ReversedSeriesIterable<S extends ImmutableSeries> extends Iterable<S> {
final List<S> seriesList;
_ReversedSeriesIterable(this.seriesList);
@override
Iterator<S> get iterator => new _ReversedSeriesIterator(seriesList);
}
/// Iterator that keeps reverse series order but keeps category order.
///
/// This is needed because for grouped stacked bars, the category stays in the
/// order it was passed in for the grouping, but the series is flipped so that
/// the first series of that category is on the top of the stack.
class _ReversedSeriesIterator<S extends ImmutableSeries> extends Iterator<S> {
final List<S> _list;
final _visitIndex = <int>[];
int _current;
_ReversedSeriesIterator(List<S> list) : _list = list {
// In the order of the list, save the category and the indices of the series
// with the same category.
final categoryAndSeriesIndexMap = <String, List<int>>{};
for (var i = 0; i < list.length; i++) {
categoryAndSeriesIndexMap
.putIfAbsent(list[i].seriesCategory, () => <int>[])
.add(i);
}
// Creates a visit that is categories in order, but the series is reversed.
categoryAndSeriesIndexMap
.forEach((_, indices) => _visitIndex.addAll(indices.reversed));
}
@override
bool moveNext() {
_current = (_current == null) ? 0 : _current + 1;
return _current < _list.length;
}
@override
S get current => _list[_visitIndex[_current]];
}

View File

@@ -0,0 +1,153 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:collection/collection.dart' show ListEquality;
import '../../common/symbol_renderer.dart'
show SymbolRenderer, RoundedRectSymbolRenderer;
import '../common/chart_canvas.dart' show FillPatternType;
import '../common/series_renderer_config.dart'
show RendererAttributes, SeriesRendererConfig;
import '../layout/layout_view.dart' show LayoutViewConfig;
/// Shared configuration for bar chart renderers.
///
/// Bar renderers support 4 different modes of rendering multiple series on the
/// chart, configured by the grouped and stacked flags.
/// * grouped - Render bars for each series that shares a domain value
/// side-by-side.
/// * stacked - Render bars for each series that shares a domain value in a
/// stack, ordered in the same order as the series list.
/// * grouped-stacked: Render bars for each series that shares a domain value in
/// a group of bar stacks. Each stack will contain all the series that share a
/// series category.
/// * floating style - When grouped and stacked are both false, all bars that
/// share a domain value will be rendered in the same domain space. Each datum
/// should be configured with a measure offset to position its bar along the
/// measure axis. Bars will freely overlap if their measure values and measure
/// offsets overlap. Note that bars for each series will be rendered in order,
/// such that bars from the last series will be "on top" of bars from previous
/// series.
abstract class BaseBarRendererConfig<D> extends LayoutViewConfig
implements SeriesRendererConfig<D> {
final String customRendererId;
final SymbolRenderer symbolRenderer;
/// Dash pattern for the stroke line around the edges of the bar.
final List<int> dashPattern;
/// Defines the way multiple series of bars are rendered per domain.
final BarGroupingType groupingType;
/// The order to paint this renderer on the canvas.
final int layoutPaintOrder;
final int minBarLengthPx;
final FillPatternType fillPattern;
final double stackHorizontalSeparator;
/// Stroke width of the target line.
final double strokeWidthPx;
/// Sets the series weight pattern. This is a pattern of weights used to
/// calculate the width of bars within a bar group. If not specified, each bar
/// in the group will have an equal width.
///
/// The pattern will not repeat. If more series are assigned to the renderer
/// than there are segments in the weight pattern, an error will be thrown.
///
/// e.g. For the pattern [2, 1], the first bar in a group should be rendered
/// twice as wide as the second bar.
///
/// If the expected bar width of the chart is 12px, then the first bar will
/// render at 16px and the second will render at 8px. The default weight
/// pattern of null means that all bars should be the same width, or 12px in
/// this case.
///
/// Not used for stacked bars.
final List<int> weightPattern;
final rendererAttributes = new RendererAttributes();
BaseBarRendererConfig(
{this.customRendererId,
this.dashPattern,
this.groupingType = BarGroupingType.grouped,
this.layoutPaintOrder,
this.minBarLengthPx = 0,
this.fillPattern,
this.stackHorizontalSeparator,
this.strokeWidthPx = 0.0,
SymbolRenderer symbolRenderer,
this.weightPattern})
: this.symbolRenderer = symbolRenderer ?? new RoundedRectSymbolRenderer();
/// Whether or not the bars should be organized into groups.
bool get grouped =>
groupingType == BarGroupingType.grouped ||
groupingType == BarGroupingType.groupedStacked;
/// Whether or not the bars should be organized into stacks.
bool get stacked =>
groupingType == BarGroupingType.stacked ||
groupingType == BarGroupingType.groupedStacked;
@override
bool operator ==(other) {
if (identical(this, other)) {
return true;
}
if (!(other is BaseBarRendererConfig)) {
return false;
}
return other.customRendererId == customRendererId &&
other.dashPattern == dashPattern &&
other.fillPattern == fillPattern &&
other.groupingType == groupingType &&
other.minBarLengthPx == minBarLengthPx &&
other.stackHorizontalSeparator == stackHorizontalSeparator &&
other.strokeWidthPx == strokeWidthPx &&
other.symbolRenderer == symbolRenderer &&
new ListEquality().equals(other.weightPattern, weightPattern);
}
int get hashcode {
var hash = 1;
hash = hash * 31 + (customRendererId?.hashCode ?? 0);
hash = hash * 31 + (dashPattern?.hashCode ?? 0);
hash = hash * 31 + (fillPattern?.hashCode ?? 0);
hash = hash * 31 + (groupingType?.hashCode ?? 0);
hash = hash * 31 + (minBarLengthPx?.hashCode ?? 0);
hash = hash * 31 + (stackHorizontalSeparator?.hashCode ?? 0);
hash = hash * 31 + (strokeWidthPx?.hashCode ?? 0);
hash = hash * 31 + (symbolRenderer?.hashCode ?? 0);
hash = hash * 31 + (weightPattern?.hashCode ?? 0);
return hash;
}
}
/// Defines the way multiple series of bars are renderered per domain.
///
/// * [grouped] - Render bars for each series that shares a domain value
/// side-by-side.
/// * [stacked] - Render bars for each series that shares a domain value in a
/// stack, ordered in the same order as the series list.
/// * [groupedStacked]: Render bars for each series that shares a domain value
/// in a group of bar stacks. Each stack will contain all the series that
/// share a series category.
enum BarGroupingType { grouped, groupedStacked, stacked }

View File

@@ -0,0 +1,129 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../common/color.dart' show Color;
import '../common/chart_canvas.dart' show getAnimatedColor, FillPatternType;
import '../common/processed_series.dart' show ImmutableSeries;
abstract class BaseBarRendererElement {
int barStackIndex;
Color color;
num cumulativeTotal;
List<int> dashPattern;
Color fillColor;
FillPatternType fillPattern;
double measureAxisPosition;
num measureOffset;
num measureOffsetPlusMeasure;
double strokeWidthPx;
bool measureIsNull;
bool measureIsNegative;
BaseBarRendererElement();
BaseBarRendererElement.clone(BaseBarRendererElement other) {
barStackIndex = other.barStackIndex;
color =
other.color != null ? new Color.fromOther(color: other.color) : null;
cumulativeTotal = other.cumulativeTotal;
dashPattern = other.dashPattern;
fillColor = other.fillColor != null
? new Color.fromOther(color: other.fillColor)
: null;
fillPattern = other.fillPattern;
measureAxisPosition = other.measureAxisPosition;
measureOffset = other.measureOffset;
measureOffsetPlusMeasure = other.measureOffsetPlusMeasure;
strokeWidthPx = other.strokeWidthPx;
measureIsNull = other.measureIsNull;
measureIsNegative = other.measureIsNegative;
}
void updateAnimationPercent(BaseBarRendererElement previous,
BaseBarRendererElement target, double animationPercent) {
color = getAnimatedColor(previous.color, target.color, animationPercent);
fillColor = getAnimatedColor(
previous.fillColor, target.fillColor, animationPercent);
measureIsNull = target.measureIsNull;
measureIsNegative = target.measureIsNegative;
}
}
abstract class BaseAnimatedBar<D, R extends BaseBarRendererElement> {
final String key;
dynamic datum;
ImmutableSeries<D> series;
D domainValue;
R _previousBar;
R _targetBar;
R _currentBar;
// Flag indicating whether this bar is being animated out of the chart.
bool animatingOut = false;
BaseAnimatedBar({this.key, this.datum, this.series, this.domainValue});
/// Animates a bar that was removed from the series out of the view.
///
/// This should be called in place of "setNewTarget" for bars that represent
/// data that has been removed from the series.
///
/// Animates the height of the bar down to the measure axis position (position
/// of 0). Animates the width of the bar down to 0, centered in the middle of
/// the original bar width.
void animateOut() {
var newTarget = clone(_currentBar);
animateElementToMeasureAxisPosition(newTarget);
setNewTarget(newTarget);
animatingOut = true;
}
/// Sets the bounds for the target to the measure axis position.
void animateElementToMeasureAxisPosition(R target);
/// Sets a new element to render.
void setNewTarget(R newTarget) {
animatingOut = false;
_currentBar ??= clone(newTarget);
_previousBar = clone(_currentBar);
_targetBar = newTarget;
}
R get currentBar => _currentBar;
R get previousBar => _previousBar;
R get targetBar => _targetBar;
/// Gets the new state of the bar element for painting, updated for a
/// transition between the previous state and the new animationPercent.
R getCurrentBar(double animationPercent) {
if (animationPercent == 1.0 || _previousBar == null) {
_currentBar = _targetBar;
_previousBar = _targetBar;
return _currentBar;
}
_currentBar.updateAnimationPercent(
_previousBar, _targetBar, animationPercent);
return _currentBar;
}
R clone(R bar);
}

View File

@@ -0,0 +1,594 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle, min, max;
import 'package:meta/meta.dart' show protected, visibleForTesting;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/text_element.dart' show TextElement;
import '../../../data/series.dart' show AttributeKey;
import '../../common/chart_canvas.dart' show ChartCanvas;
import '../../common/chart_context.dart' show ChartContext;
import '../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
ViewMeasuredSizes;
import 'axis_tick.dart' show AxisTicks;
import 'draw_strategy/small_tick_draw_strategy.dart' show SmallTickDrawStrategy;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'linear/linear_scale.dart' show LinearScale;
import 'numeric_extents.dart' show NumericExtents;
import 'numeric_scale.dart' show NumericScale;
import 'numeric_tick_provider.dart' show NumericTickProvider;
import 'ordinal_tick_provider.dart' show OrdinalTickProvider;
import 'scale.dart'
show MutableScale, RangeBandConfig, ScaleOutputExtent, Scale;
import 'simple_ordinal_scale.dart' show SimpleOrdinalScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart'
show TickFormatter, OrdinalTickFormatter, NumericTickFormatter;
import 'tick_provider.dart' show TickProvider;
const measureAxisIdKey = const AttributeKey<String>('Axis.measureAxisId');
const measureAxisKey = const AttributeKey<Axis>('Axis.measureAxis');
const domainAxisKey = const AttributeKey<Axis>('Axis.domainAxis');
/// Orientation of an Axis.
enum AxisOrientation { top, right, bottom, left }
abstract class ImmutableAxis<D> {
/// Compare domain to the viewport.
///
/// 0 if the domain is in the viewport.
/// 1 if the domain is to the right of the viewport.
/// -1 if the domain is to the left of the viewport.
int compareDomainValueToViewport(D domain);
/// Get location for the domain.
double getLocation(D domain);
D getDomain(double location);
/// Rangeband for this axis.
double get rangeBand;
/// Step size for this axis.
double get stepSize;
/// Output range for this axis.
ScaleOutputExtent get range;
}
abstract class Axis<D> extends ImmutableAxis<D> implements LayoutView {
static const primaryMeasureAxisId = 'primaryMeasureAxisId';
static const secondaryMeasureAxisId = 'secondaryMeasureAxisId';
final MutableScale<D> _scale;
/// [Scale] of this axis.
@protected
MutableScale<D> get scale => _scale;
/// Previous [Scale] of this axis, used to calculate tick animation.
MutableScale<D> _previousScale;
TickProvider<D> _tickProvider;
/// [TickProvider] for this axis.
TickProvider<D> get tickProvider => _tickProvider;
set tickProvider(TickProvider<D> tickProvider) {
_tickProvider = tickProvider;
}
/// [TickFormatter] for this axis.
TickFormatter<D> _tickFormatter;
set tickFormatter(TickFormatter<D> formatter) {
if (_tickFormatter != formatter) {
_tickFormatter = formatter;
_formatterValueCache.clear();
}
}
TickFormatter<D> get tickFormatter => _tickFormatter;
final _formatterValueCache = <D, String>{};
/// [TickDrawStrategy] for this axis.
TickDrawStrategy<D> tickDrawStrategy;
/// [AxisOrientation] for this axis.
AxisOrientation axisOrientation;
ChartContext context;
/// If the output range should be reversed.
bool reverseOutputRange = false;
/// Whether or not the axis will configure the viewport to have "niced" ticks
/// around the domain values.
bool _autoViewport = true;
/// If the axis line should always be drawn.
bool forceDrawAxisLine;
/// If true, do not allow axis to be modified.
///
/// Ticks (including their location) are not updated.
/// Viewport changes not allowed.
bool lockAxis = false;
/// Ticks provided by the tick provider.
List<Tick> _providedTicks;
/// Ticks used by the axis for drawing.
final _axisTicks = <AxisTicks<D>>[];
Rectangle<int> _componentBounds;
Rectangle<int> _drawAreaBounds;
GraphicsFactory _graphicsFactory;
/// Order for chart layout painting.
///
/// In general, domain axes should be drawn on top of measure axes to ensure
/// that the domain axis line appears on top of any measure axis grid lines.
int layoutPaintOrder = LayoutViewPaintOrder.measureAxis;
Axis(
{TickProvider<D> tickProvider,
TickFormatter<D> tickFormatter,
MutableScale<D> scale})
: this._scale = scale,
this._tickProvider = tickProvider,
this._tickFormatter = tickFormatter;
@protected
MutableScale<D> get mutableScale => _scale;
/// Rangeband for this axis.
@override
double get rangeBand => _scale.rangeBand;
@override
double get stepSize => _scale.stepSize;
@override
ScaleOutputExtent get range => _scale.range;
/// Configures whether the viewport should be reset back to default values
/// when the domain is reset.
///
/// This should generally be disabled when the viewport will be managed
/// externally, e.g. from pan and zoom behaviors.
set autoViewport(bool autoViewport) {
_autoViewport = autoViewport;
}
bool get autoViewport => _autoViewport;
void setRangeBandConfig(RangeBandConfig rangeBandConfig) {
mutableScale.rangeBandConfig = rangeBandConfig;
}
void addDomainValue(D domain) {
if (lockAxis) {
return;
}
_scale.addDomain(domain);
}
void resetDomains() {
if (lockAxis) {
return;
}
// If the series list changes, clear the cache.
//
// There are cases where tick formatter has not "changed", but if measure
// formatter provided to the tick formatter uses a closure value, the
// formatter cache needs to be cleared.
//
// This type of use case for the measure formatter surfaced where the series
// list also changes. So this is a round about way to also clear the
// tick formatter cache.
//
// TODO: Measure formatter should be changed from a typedef to
// a concrete class to force users to create a new tick formatter when
// formatting is different, so we can recognize when the tick formatter is
// changed and then clear cache accordingly.
//
// Remove this when bug above is fixed, and verify it did not cause
// regression for b/110371453.
_formatterValueCache.clear();
_scale.resetDomain();
reverseOutputRange = false;
if (_autoViewport) {
_scale.resetViewportSettings();
}
// TODO: Reset rangeband and step size when we port over config
//scale.rangeBandConfig = get range band config
//scale.stepSizeConfig = get step size config
}
@override
double getLocation(D domain) => domain != null ? _scale[domain] : null;
@override
D getDomain(double location) => _scale.reverse(location);
@override
int compareDomainValueToViewport(D domain) {
return _scale.compareDomainValueToViewport(domain);
}
void setOutputRange(int start, int end) {
_scale.range = new ScaleOutputExtent(start, end);
}
/// Request update ticks from tick provider and update the painted ticks.
void updateTicks() {
_updateProvidedTicks();
_updateAxisTicks();
}
/// Request ticks from tick provider.
void _updateProvidedTicks() {
if (lockAxis) {
return;
}
// TODO: Ensure that tick providers take manually configured
// viewport settings into account, so that we still get the right number.
_providedTicks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: _scale,
formatter: tickFormatter,
formatterValueCache: _formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
orientation: axisOrientation,
viewportExtensionEnabled: _autoViewport);
}
/// Updates the ticks that are actually used for drawing.
void _updateAxisTicks() {
if (lockAxis) {
return;
}
final providedTicks = new List.from(_providedTicks ?? []);
for (AxisTicks<D> animatedTick in _axisTicks) {
final tick = providedTicks?.firstWhere(
(t) => t.value == animatedTick.value,
orElse: () => null);
if (tick != null) {
// Swap out the text element only if the settings are different.
// This prevents a costly new TextPainter in Flutter.
if (!TextElement.elementSettingsSame(
animatedTick.textElement, tick.textElement)) {
animatedTick.textElement = tick.textElement;
}
// Update target for all existing ticks
animatedTick.setNewTarget(_scale[tick.value]);
providedTicks.remove(tick);
} else {
// Animate out ticks that do not exist any more.
animatedTick.animateOut(_scale[animatedTick.value].toDouble());
}
}
// Add new ticks
providedTicks?.forEach((tick) {
final animatedTick = new AxisTicks<D>(tick);
if (_previousScale != null) {
animatedTick.animateInFrom(_previousScale[tick.value].toDouble());
}
_axisTicks.add(animatedTick);
});
_axisTicks.sort();
// Save a copy of the current scale to be used as the previous scale when
// ticks are updated.
_previousScale = _scale.copy();
}
/// Configures the zoom and translate.
///
/// [viewportScale] is the zoom factor to use, likely >= 1.0 where 1.0 maps
/// the complete data extents to the output range, and 2.0 only maps half the
/// data to the output range.
///
/// [viewportTranslatePx] is the translate/pan to use in pixel units,
/// likely <= 0 which shifts the start of the data before the edge of the
/// chart giving us a pan.
///
/// [drawAreaWidth] is the width of the draw area for the series data in pixel
/// units, at minimum viewport scale level (1.0). When provided,
/// [viewportTranslatePx] will be clamped such that the axis cannot be panned
/// beyond the bounds of the data.
void setViewportSettings(double viewportScale, double viewportTranslatePx,
{int drawAreaWidth}) {
// Don't let the viewport be panned beyond the bounds of the data.
viewportTranslatePx = _clampTranslatePx(viewportScale, viewportTranslatePx,
drawAreaWidth: drawAreaWidth);
_scale.setViewportSettings(viewportScale, viewportTranslatePx);
}
/// Returns the current viewport scale.
///
/// A scale of 1.0 would map the data directly to the output range, while a
/// value of 2.0 would map the data to an output of double the range so you
/// only see half the data in the viewport. This is the equivalent to
/// zooming. Its value is likely >= 1.0.
double get viewportScalingFactor => _scale.viewportScalingFactor;
/// Returns the current pixel viewport offset
///
/// The translate is used by the scale function when it applies the scale.
/// This is the equivalent to panning. Its value is likely <= 0 to pan the
/// data to the left.
double get viewportTranslatePx => _scale?.viewportTranslatePx;
/// Clamps a possible change in domain translation to fit within the range of
/// the data.
double _clampTranslatePx(
double viewportScalingFactor, double viewportTranslatePx,
{int drawAreaWidth}) {
if (drawAreaWidth == null) {
return viewportTranslatePx;
}
// Bound the viewport translate to the range of the data.
final maxNegativeTranslate =
-1.0 * ((drawAreaWidth * viewportScalingFactor) - drawAreaWidth);
viewportTranslatePx =
min(max(viewportTranslatePx, maxNegativeTranslate), 0.0);
return viewportTranslatePx;
}
//
// LayoutView methods.
//
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
@override
LayoutViewConfig get layoutConfig => new LayoutViewConfig(
paintOrder: layoutPaintOrder,
position: _layoutPosition,
positionOrder: LayoutViewPositionOrder.axis);
/// Get layout position from axis orientation.
LayoutPosition get _layoutPosition {
LayoutPosition position;
switch (axisOrientation) {
case AxisOrientation.top:
position = LayoutPosition.Top;
break;
case AxisOrientation.right:
position = LayoutPosition.Right;
break;
case AxisOrientation.bottom:
position = LayoutPosition.Bottom;
break;
case AxisOrientation.left:
position = LayoutPosition.Left;
break;
}
return position;
}
/// The axis is rendered vertically.
bool get isVertical =>
axisOrientation == AxisOrientation.left ||
axisOrientation == AxisOrientation.right;
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
return isVertical
? _measureVerticalAxis(maxWidth, maxHeight)
: _measureHorizontalAxis(maxWidth, maxHeight);
}
ViewMeasuredSizes _measureVerticalAxis(int maxWidth, int maxHeight) {
setOutputRange(maxHeight, 0);
_updateProvidedTicks();
return tickDrawStrategy.measureVerticallyDrawnTicks(
_providedTicks, maxWidth, maxHeight);
}
ViewMeasuredSizes _measureHorizontalAxis(int maxWidth, int maxHeight) {
setOutputRange(0, maxWidth);
_updateProvidedTicks();
return tickDrawStrategy.measureHorizontallyDrawnTicks(
_providedTicks, maxWidth, maxHeight);
}
/// Layout this component.
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
_componentBounds = componentBounds;
_drawAreaBounds = drawAreaBounds;
// Update the output range if it is different than the current one.
// This is necessary because during the measure cycle, the output range is
// set between zero and the max range available. On layout, the output range
// needs to be updated to account of the offset of the axis view.
final outputStart =
isVertical ? _componentBounds.bottom : _componentBounds.left;
final outputEnd =
isVertical ? _componentBounds.top : _componentBounds.right;
final outputRange = reverseOutputRange
? new ScaleOutputExtent(outputEnd, outputStart)
: new ScaleOutputExtent(outputStart, outputEnd);
if (_scale.range != outputRange) {
_scale.range = outputRange;
}
_updateProvidedTicks();
// Update animated ticks in layout, because updateTicks are called during
// measure and we don't want to update the animation at that time.
_updateAxisTicks();
}
@override
bool get isSeriesRenderer => false;
@override
Rectangle<int> get componentBounds => this._componentBounds;
bool get drawAxisLine {
if (forceDrawAxisLine != null) {
return forceDrawAxisLine;
}
return tickDrawStrategy is SmallTickDrawStrategy;
}
@override
void paint(ChartCanvas canvas, double animationPercent) {
if (animationPercent == 1.0) {
_axisTicks.removeWhere((t) => t.markedForRemoval);
}
for (var i = 0; i < _axisTicks.length; i++) {
final animatedTick = _axisTicks[i];
tickDrawStrategy.draw(
canvas, animatedTick..setCurrentTick(animationPercent),
orientation: axisOrientation,
axisBounds: _componentBounds,
drawAreaBounds: _drawAreaBounds,
isFirst: i == 0,
isLast: i == _axisTicks.length - 1);
}
if (drawAxisLine) {
tickDrawStrategy.drawAxisLine(canvas, axisOrientation, _componentBounds);
}
}
}
class NumericAxis extends Axis<num> {
NumericAxis({TickProvider<num> tickProvider})
: super(
tickProvider: tickProvider ?? new NumericTickProvider(),
tickFormatter: new NumericTickFormatter(),
scale: new LinearScale(),
);
void setScaleViewport(NumericExtents viewport) {
autoViewport = false;
(_scale as NumericScale).viewportDomain = viewport;
}
}
class OrdinalAxis extends Axis<String> {
OrdinalAxis({
TickDrawStrategy tickDrawStrategy,
TickProvider tickProvider,
TickFormatter tickFormatter,
}) : super(
tickProvider: tickProvider ?? const OrdinalTickProvider(),
tickFormatter: tickFormatter ?? const OrdinalTickFormatter(),
scale: new SimpleOrdinalScale(),
);
void setScaleViewport(OrdinalViewport viewport) {
autoViewport = false;
(_scale as SimpleOrdinalScale)
.setViewport(viewport.dataSize, viewport.startingDomain);
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
super.layout(componentBounds, drawAreaBounds);
// We are purposely clearing the viewport starting domain and data size
// post layout.
//
// Originally we set a flag in [setScaleViewport] to recalculate viewport
// settings on next scale update and then reset the flag. This doesn't work
// because chart's measure cycle provides different ranges to the scale,
// causing the scale to update multiple times before it is finalized after
// layout.
//
// By resetting the viewport after layout, we guarantee the correct range
// was used to apply the viewport and behaviors that update the viewport
// based on translate and scale changes will not be affected (pan/zoom).
(_scale as SimpleOrdinalScale).setViewport(null, null);
}
}
/// Viewport to cover [dataSize] data points starting at [startingDomain] value.
class OrdinalViewport {
final String startingDomain;
final int dataSize;
OrdinalViewport(this.startingDomain, this.dataSize);
@override
bool operator ==(Object other) {
return other is OrdinalViewport &&
startingDomain == other.startingDomain &&
dataSize == other.dataSize;
}
@override
int get hashCode {
int hashcode = startingDomain.hashCode;
hashcode = (hashcode * 37) + dataSize;
return hashcode;
}
}
@visibleForTesting
class AxisTester<D> {
final Axis<D> _axis;
AxisTester(this._axis);
List<AxisTicks<D>> get axisTicks => _axis._axisTicks;
MutableScale<D> get scale => _axis._scale;
List<D> get axisValues => axisTicks.map((t) => t.value).toList();
}

View File

@@ -0,0 +1,112 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'tick.dart' show Tick;
class AxisTicks<D> extends Tick<D> implements Comparable<AxisTicks<D>> {
/// This tick is being animated out.
bool _markedForRemoval;
/// This tick's current location.
double _currentLocation;
/// This tick's previous target location.
double _previousLocation;
/// This tick's current target location.
double _targetLocation;
/// This tick's current opacity.
double _currentOpacity;
/// This tick's previous opacity.
double _previousOpacity;
/// This tick's target opacity.
double _targetOpacity;
AxisTicks(Tick<D> tick)
: super(
value: tick.value,
textElement: tick.textElement,
locationPx: tick.locationPx,
labelOffsetPx: tick.labelOffsetPx) {
/// Set the initial target for a new animated tick.
_markedForRemoval = false;
_targetLocation = tick.locationPx;
}
bool get markedForRemoval => _markedForRemoval;
/// Animate the tick in from [previousLocation].
void animateInFrom(double previousLocation) {
_markedForRemoval = false;
_previousLocation = previousLocation;
_previousOpacity = 0.0;
_targetOpacity = 1.0;
}
/// Animate out this tick to [newLocation].
void animateOut(double newLocation) {
_markedForRemoval = true;
_previousLocation = _currentLocation;
_targetLocation = newLocation;
_previousOpacity = _currentOpacity;
_targetOpacity = 0.0;
}
/// Set new target for this tick to be [newLocation].
void setNewTarget(double newLocation) {
_markedForRemoval = false;
_previousLocation = _currentLocation;
_targetLocation = newLocation;
_previousOpacity = _currentOpacity;
_targetOpacity = 1.0;
}
/// Update tick's location and opacity based on animation percent.
void setCurrentTick(double animationPercent) {
if (animationPercent == 1.0) {
_currentLocation = _targetLocation;
_previousLocation = _targetLocation;
_currentOpacity = markedForRemoval ? 0.0 : 1.0;
} else if (_previousLocation == null) {
_currentLocation = _targetLocation;
_currentOpacity = 1.0;
} else {
_currentLocation =
_lerpDouble(_previousLocation, _targetLocation, animationPercent);
_currentOpacity =
_lerpDouble(_previousOpacity, _targetOpacity, animationPercent);
}
locationPx = _currentLocation;
textElement.opacity = _currentOpacity;
}
/// Linearly interpolate between two numbers.
///
/// From lerpDouble in dart:ui which is Flutter only.
double _lerpDouble(double a, double b, double t) {
if (a == null && b == null) return null;
a ??= 0.0;
b ??= 0.0;
return a + (b - a) * t;
}
int compareTo(AxisTicks<D> other) {
return _targetLocation.compareTo(other._targetLocation);
}
}

View File

@@ -0,0 +1,38 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import 'tick.dart' show Tick;
/// A report that contains a list of ticks and if they collide.
class CollisionReport {
/// If [ticks] collide.
final bool ticksCollide;
final List<Tick> ticks;
final bool alternateTicksUsed;
CollisionReport(
{@required this.ticksCollide,
@required this.ticks,
bool alternateTicksUsed})
: alternateTicksUsed = alternateTicksUsed ?? false;
CollisionReport.empty()
: ticksCollide = false,
ticks = [],
alternateTicksUsed = false;
}

View File

@@ -0,0 +1,436 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:meta/meta.dart' show immutable, protected, required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/line_style.dart' show LineStyle;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../../common/text_element.dart' show TextDirection;
import '../../../../common/text_style.dart' show TextStyle;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../common/chart_context.dart' show ChartContext;
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
import '../axis.dart' show AxisOrientation;
import '../collision_report.dart' show CollisionReport;
import '../spec/axis_spec.dart'
show
TextStyleSpec,
TickLabelAnchor,
TickLabelJustification,
LineStyleSpec,
RenderSpec;
import '../tick.dart' show Tick;
import 'tick_draw_strategy.dart' show TickDrawStrategy;
@immutable
abstract class BaseRenderSpec<D> implements RenderSpec<D> {
final TextStyleSpec labelStyle;
final TickLabelAnchor labelAnchor;
final TickLabelJustification labelJustification;
final int labelOffsetFromAxisPx;
/// Absolute distance from the tick to the text if using start/end
final int labelOffsetFromTickPx;
final int minimumPaddingBetweenLabelsPx;
final LineStyleSpec axisLineStyle;
const BaseRenderSpec({
this.labelStyle,
this.labelAnchor,
this.labelJustification,
this.labelOffsetFromAxisPx,
this.labelOffsetFromTickPx,
this.minimumPaddingBetweenLabelsPx,
this.axisLineStyle,
});
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is BaseRenderSpec &&
labelStyle == other.labelStyle &&
labelAnchor == other.labelAnchor &&
labelJustification == other.labelJustification &&
labelOffsetFromTickPx == other.labelOffsetFromTickPx &&
labelOffsetFromAxisPx == other.labelOffsetFromAxisPx &&
minimumPaddingBetweenLabelsPx ==
other.minimumPaddingBetweenLabelsPx &&
axisLineStyle == other.axisLineStyle);
}
@override
int get hashCode {
int hashcode = labelStyle?.hashCode ?? 0;
hashcode = (hashcode * 37) + labelAnchor?.hashCode ?? 0;
hashcode = (hashcode * 37) + labelJustification?.hashCode ?? 0;
hashcode = (hashcode * 37) + labelOffsetFromTickPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + labelOffsetFromAxisPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + minimumPaddingBetweenLabelsPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + axisLineStyle?.hashCode ?? 0;
return hashcode;
}
}
/// Base strategy that draws tick labels and checks for label collisions.
abstract class BaseTickDrawStrategy<D> implements TickDrawStrategy<D> {
final ChartContext chartContext;
LineStyle axisLineStyle;
TextStyle labelStyle;
TickLabelAnchor tickLabelAnchor;
TickLabelJustification tickLabelJustification;
int labelOffsetFromAxisPx;
int labelOffsetFromTickPx;
int minimumPaddingBetweenLabelsPx;
BaseTickDrawStrategy(this.chartContext, GraphicsFactory graphicsFactory,
{TextStyleSpec labelStyleSpec,
LineStyleSpec axisLineStyleSpec,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
int minimumPaddingBetweenLabelsPx}) {
labelStyle = (graphicsFactory.createTextPaint()
..color = labelStyleSpec?.color ?? StyleFactory.style.tickColor
..fontFamily = labelStyleSpec?.fontFamily
..fontSize = labelStyleSpec?.fontSize ?? 12);
axisLineStyle = graphicsFactory.createLinePaint()
..color = axisLineStyleSpec?.color ?? labelStyle.color
..dashPattern = axisLineStyleSpec?.dashPattern
..strokeWidth = axisLineStyleSpec?.thickness ?? 1;
tickLabelAnchor = labelAnchor ?? TickLabelAnchor.centered;
tickLabelJustification =
labelJustification ?? TickLabelJustification.inside;
this.labelOffsetFromAxisPx = labelOffsetFromAxisPx ?? 5;
this.labelOffsetFromTickPx = labelOffsetFromTickPx ?? 5;
this.minimumPaddingBetweenLabelsPx = minimumPaddingBetweenLabelsPx ?? 50;
}
@override
void decorateTicks(List<Tick<D>> ticks) {
for (Tick<D> tick in ticks) {
// If no style at all, set the default style.
if (tick.textElement.textStyle == null) {
tick.textElement.textStyle = labelStyle;
} else {
//Fill in whatever is missing
tick.textElement.textStyle.color ??= labelStyle.color;
tick.textElement.textStyle.fontFamily ??= labelStyle.fontFamily;
tick.textElement.textStyle.fontSize ??= labelStyle.fontSize;
}
}
}
@override
CollisionReport collides(List<Tick<D>> ticks, AxisOrientation orientation) {
// If there are no ticks, they do not collide.
if (ticks == null) {
return new CollisionReport(
ticksCollide: false, ticks: ticks, alternateTicksUsed: false);
}
final vertical = orientation == AxisOrientation.left ||
orientation == AxisOrientation.right;
// First sort ticks by smallest locationPx first (NOT sorted by value).
// This allows us to only check if a tick collides with the previous tick.
ticks.sort((a, b) {
if (a.locationPx < b.locationPx) {
return -1;
} else if (a.locationPx > b.locationPx) {
return 1;
} else {
return 0;
}
});
double previousEnd = double.negativeInfinity;
bool collides = false;
for (final tick in ticks) {
final tickSize = tick.textElement.measurement;
if (vertical) {
final adjustedHeight =
tickSize.verticalSliceWidth + minimumPaddingBetweenLabelsPx;
if (tickLabelAnchor == TickLabelAnchor.inside) {
if (identical(tick, ticks.first)) {
// Top most tick draws down from the location
collides = false;
previousEnd = tick.locationPx + adjustedHeight;
} else if (identical(tick, ticks.last)) {
// Bottom most tick draws up from the location
collides = previousEnd > tick.locationPx - adjustedHeight;
previousEnd = tick.locationPx;
} else {
// All other ticks is centered.
final halfHeight = adjustedHeight / 2;
collides = previousEnd > tick.locationPx - halfHeight;
previousEnd = tick.locationPx + halfHeight;
}
} else {
collides = previousEnd > tick.locationPx;
previousEnd = tick.locationPx + adjustedHeight;
}
} else {
// Use the text direction the ticks specified, unless the label anchor
// is set to [TickLabelAnchor.inside]. When 'inside' is set, the text
// direction is normalized such that the left most tick is drawn ltr,
// the last tick is drawn rtl, and all other ticks are in the center.
// This is not set until it is painted, so collision check needs to get
// the value also.
final textDirection = _normalizeHorizontalAnchor(
tickLabelAnchor,
chartContext.isRtl,
identical(tick, ticks.first),
identical(tick, ticks.last));
final adjustedWidth =
tickSize.horizontalSliceWidth + minimumPaddingBetweenLabelsPx;
switch (textDirection) {
case TextDirection.ltr:
collides = previousEnd > tick.locationPx;
previousEnd = tick.locationPx + adjustedWidth;
break;
case TextDirection.rtl:
collides = previousEnd > (tick.locationPx - adjustedWidth);
previousEnd = tick.locationPx;
break;
case TextDirection.center:
final halfWidth = adjustedWidth / 2;
collides = previousEnd > tick.locationPx - halfWidth;
previousEnd = tick.locationPx + halfWidth;
break;
}
}
if (collides) {
return new CollisionReport(
ticksCollide: true, ticks: ticks, alternateTicksUsed: false);
}
}
return new CollisionReport(
ticksCollide: false, ticks: ticks, alternateTicksUsed: false);
}
@override
ViewMeasuredSizes measureVerticallyDrawnTicks(
List<Tick<D>> ticks, int maxWidth, int maxHeight) {
// TODO: Add spacing to account for the distance between the
// text and the axis baseline (even if it isn't drawn).
final maxHorizontalSliceWidth = ticks
.fold(
0.0,
(double prevMax, tick) => max(
prevMax,
tick.textElement.measurement.horizontalSliceWidth +
labelOffsetFromAxisPx))
.round();
return new ViewMeasuredSizes(
preferredWidth: maxHorizontalSliceWidth, preferredHeight: maxHeight);
}
@override
ViewMeasuredSizes measureHorizontallyDrawnTicks(
List<Tick<D>> ticks, int maxWidth, int maxHeight) {
final maxVerticalSliceWidth = ticks
.fold(
0.0,
(double prevMax, tick) =>
max(prevMax, tick.textElement.measurement.verticalSliceWidth))
.round();
return new ViewMeasuredSizes(
preferredWidth: maxWidth,
preferredHeight: maxVerticalSliceWidth + labelOffsetFromAxisPx);
}
@override
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
Rectangle<int> axisBounds) {
Point<num> start;
Point<num> end;
switch (orientation) {
case AxisOrientation.top:
start = axisBounds.bottomLeft;
end = axisBounds.bottomRight;
break;
case AxisOrientation.bottom:
start = axisBounds.topLeft;
end = axisBounds.topRight;
break;
case AxisOrientation.right:
start = axisBounds.topLeft;
end = axisBounds.bottomLeft;
break;
case AxisOrientation.left:
start = axisBounds.topRight;
end = axisBounds.bottomRight;
break;
}
canvas.drawLine(
points: [start, end],
fill: axisLineStyle.color,
stroke: axisLineStyle.color,
strokeWidthPx: axisLineStyle.strokeWidth.toDouble(),
dashPattern: axisLineStyle.dashPattern,
);
}
@protected
void drawLabel(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast}) {
final locationPx = tick.locationPx;
final measurement = tick.textElement.measurement;
final isRtl = chartContext.isRtl;
int x = 0;
int y = 0;
final labelOffsetPx = tick.labelOffsetPx ?? 0;
if (orientation == AxisOrientation.bottom ||
orientation == AxisOrientation.top) {
y = orientation == AxisOrientation.bottom
? axisBounds.top + labelOffsetFromAxisPx
: axisBounds.bottom -
measurement.verticalSliceWidth.toInt() -
labelOffsetFromAxisPx;
final direction =
_normalizeHorizontalAnchor(tickLabelAnchor, isRtl, isFirst, isLast);
tick.textElement.textDirection = direction;
switch (direction) {
case TextDirection.rtl:
x = (locationPx + labelOffsetFromTickPx + labelOffsetPx).toInt();
break;
case TextDirection.ltr:
x = (locationPx - labelOffsetFromTickPx - labelOffsetPx).toInt();
break;
case TextDirection.center:
default:
x = (locationPx - labelOffsetPx).toInt();
break;
}
} else {
if (orientation == AxisOrientation.left) {
if (tickLabelJustification == TickLabelJustification.inside) {
x = axisBounds.right - labelOffsetFromAxisPx;
tick.textElement.textDirection = TextDirection.rtl;
} else {
x = axisBounds.left + labelOffsetFromAxisPx;
tick.textElement.textDirection = TextDirection.ltr;
}
} else {
// orientation == right
if (tickLabelJustification == TickLabelJustification.inside) {
x = axisBounds.left + labelOffsetFromAxisPx;
tick.textElement.textDirection = TextDirection.ltr;
} else {
x = axisBounds.right - labelOffsetFromAxisPx;
tick.textElement.textDirection = TextDirection.rtl;
}
}
switch (_normalizeVerticalAnchor(tickLabelAnchor, isFirst, isLast)) {
case _PixelVerticalDirection.over:
y = (locationPx -
measurement.verticalSliceWidth -
labelOffsetFromTickPx -
labelOffsetPx)
.toInt();
break;
case _PixelVerticalDirection.under:
y = (locationPx + labelOffsetFromTickPx + labelOffsetPx).toInt();
break;
case _PixelVerticalDirection.center:
default:
y = (locationPx - measurement.verticalSliceWidth / 2 + labelOffsetPx)
.toInt();
break;
}
}
canvas.drawText(tick.textElement, x, y);
}
TextDirection _normalizeHorizontalAnchor(
TickLabelAnchor anchor, bool isRtl, bool isFirst, bool isLast) {
switch (anchor) {
case TickLabelAnchor.before:
return isRtl ? TextDirection.ltr : TextDirection.rtl;
case TickLabelAnchor.after:
return isRtl ? TextDirection.rtl : TextDirection.ltr;
case TickLabelAnchor.inside:
if (isFirst) {
return TextDirection.ltr;
}
if (isLast) {
return TextDirection.rtl;
}
return TextDirection.center;
case TickLabelAnchor.centered:
default:
return TextDirection.center;
}
}
_PixelVerticalDirection _normalizeVerticalAnchor(
TickLabelAnchor anchor, bool isFirst, bool isLast) {
switch (anchor) {
case TickLabelAnchor.before:
return _PixelVerticalDirection.under;
case TickLabelAnchor.after:
return _PixelVerticalDirection.over;
case TickLabelAnchor.inside:
if (isFirst) {
return _PixelVerticalDirection.over;
}
if (isLast) {
return _PixelVerticalDirection.under;
}
return _PixelVerticalDirection.center;
case TickLabelAnchor.centered:
default:
return _PixelVerticalDirection.center;
}
}
}
enum _PixelVerticalDirection {
over,
center,
under,
}

View File

@@ -0,0 +1,174 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:meta/meta.dart' show immutable, required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/line_style.dart' show LineStyle;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../spec/axis_spec.dart'
show TextStyleSpec, LineStyleSpec, TickLabelAnchor, TickLabelJustification;
import '../tick.dart' show Tick;
import 'base_tick_draw_strategy.dart' show BaseTickDrawStrategy;
import 'small_tick_draw_strategy.dart' show SmallTickRendererSpec;
import 'tick_draw_strategy.dart' show TickDrawStrategy;
@immutable
class GridlineRendererSpec<D> extends SmallTickRendererSpec<D> {
const GridlineRendererSpec({
TextStyleSpec labelStyle,
LineStyleSpec lineStyle,
LineStyleSpec axisLineStyle,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int tickLengthPx,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
int minimumPaddingBetweenLabelsPx,
}) : super(
labelStyle: labelStyle,
lineStyle: lineStyle,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx,
tickLengthPx: tickLengthPx,
axisLineStyle: axisLineStyle);
@override
TickDrawStrategy<D> createDrawStrategy(
ChartContext context, GraphicsFactory graphicsFactory) =>
new GridlineTickDrawStrategy<D>(context, graphicsFactory,
tickLengthPx: tickLengthPx,
lineStyleSpec: lineStyle,
labelStyleSpec: labelStyle,
axisLineStyleSpec: axisLineStyle,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx);
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is GridlineRendererSpec && super == (other));
}
@override
int get hashCode {
int hashcode = super.hashCode;
return hashcode;
}
}
/// Draws line across chart draw area for each tick.
///
/// Extends [BaseTickDrawStrategy].
class GridlineTickDrawStrategy<D> extends BaseTickDrawStrategy<D> {
int tickLength;
LineStyle lineStyle;
GridlineTickDrawStrategy(
ChartContext chartContext,
GraphicsFactory graphicsFactory, {
int tickLengthPx,
LineStyleSpec lineStyleSpec,
TextStyleSpec labelStyleSpec,
LineStyleSpec axisLineStyleSpec,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
int minimumPaddingBetweenLabelsPx,
}) : super(chartContext, graphicsFactory,
labelStyleSpec: labelStyleSpec,
axisLineStyleSpec: axisLineStyleSpec ?? lineStyleSpec,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx) {
lineStyle =
StyleFactory.style.createGridlineStyle(graphicsFactory, lineStyleSpec);
this.tickLength = tickLengthPx ?? 0;
}
@override
void draw(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast}) {
Point<num> lineStart;
Point<num> lineEnd;
switch (orientation) {
case AxisOrientation.top:
final x = tick.locationPx;
lineStart = new Point(x, axisBounds.bottom - tickLength);
lineEnd = new Point(x, drawAreaBounds.bottom);
break;
case AxisOrientation.bottom:
final x = tick.locationPx;
lineStart = new Point(x, drawAreaBounds.top + tickLength);
lineEnd = new Point(x, axisBounds.top);
break;
case AxisOrientation.right:
final y = tick.locationPx;
if (tickLabelAnchor == TickLabelAnchor.after ||
tickLabelAnchor == TickLabelAnchor.before) {
lineStart = new Point(axisBounds.right, y);
} else {
lineStart = new Point(axisBounds.left + tickLength, y);
}
lineEnd = new Point(drawAreaBounds.left, y);
break;
case AxisOrientation.left:
final y = tick.locationPx;
if (tickLabelAnchor == TickLabelAnchor.after ||
tickLabelAnchor == TickLabelAnchor.before) {
lineStart = new Point(axisBounds.left, y);
} else {
lineStart = new Point(axisBounds.right - tickLength, y);
}
lineEnd = new Point(drawAreaBounds.right, y);
break;
}
canvas.drawLine(
points: [lineStart, lineEnd],
dashPattern: lineStyle.dashPattern,
fill: lineStyle.color,
stroke: lineStyle.color,
strokeWidthPx: lineStyle.strokeWidth.toDouble(),
);
drawLabel(canvas, tick,
orientation: orientation,
axisBounds: axisBounds,
drawAreaBounds: drawAreaBounds,
isFirst: isFirst,
isLast: isLast);
}
}

View File

@@ -0,0 +1,136 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:meta/meta.dart' show immutable, required;
import '../../../../common/color.dart' show Color;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/line_style.dart' show LineStyle;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../../common/text_style.dart' show TextStyle;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../common/chart_context.dart' show ChartContext;
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
import '../axis.dart' show AxisOrientation;
import '../collision_report.dart' show CollisionReport;
import '../spec/axis_spec.dart' show RenderSpec, LineStyleSpec;
import '../tick.dart' show Tick;
import 'tick_draw_strategy.dart';
/// Renders no ticks no labels, and claims no space in layout.
/// However, it does render the axis line if asked to by the axis.
@immutable
class NoneRenderSpec<D> extends RenderSpec<D> {
final LineStyleSpec axisLineStyle;
const NoneRenderSpec({this.axisLineStyle});
@override
TickDrawStrategy<D> createDrawStrategy(
ChartContext context, GraphicsFactory graphicFactory) =>
new NoneDrawStrategy<D>(context, graphicFactory,
axisLineStyleSpec: axisLineStyle);
@override
bool operator ==(Object other) =>
identical(this, other) || other is NoneRenderSpec;
@override
int get hashCode => 0;
}
class NoneDrawStrategy<D> implements TickDrawStrategy<D> {
LineStyle axisLineStyle;
TextStyle noneTextStyle;
NoneDrawStrategy(ChartContext chartContext, GraphicsFactory graphicsFactory,
{LineStyleSpec axisLineStyleSpec}) {
axisLineStyle = StyleFactory.style
.createAxisLineStyle(graphicsFactory, axisLineStyleSpec);
noneTextStyle = graphicsFactory.createTextPaint()
..color = Color.transparent
..fontSize = 0;
}
@override
CollisionReport collides(List<Tick> ticks, AxisOrientation orientation) =>
new CollisionReport(ticksCollide: false, ticks: ticks);
@override
void decorateTicks(List<Tick> ticks) {
// Even though no text is rendered, the text style for each element should
// still be set to handle the case of the draw strategy being switched to
// a different draw strategy. The new draw strategy will try to animate
// the old ticks out and the text style property is used.
ticks.forEach((tick) => tick.textElement.textStyle = noneTextStyle);
}
@override
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
Rectangle<int> axisBounds) {
Point<num> start;
Point<num> end;
switch (orientation) {
case AxisOrientation.top:
start = axisBounds.bottomLeft;
end = axisBounds.bottomRight;
break;
case AxisOrientation.bottom:
start = axisBounds.topLeft;
end = axisBounds.topRight;
break;
case AxisOrientation.right:
start = axisBounds.topLeft;
end = axisBounds.bottomLeft;
break;
case AxisOrientation.left:
start = axisBounds.topRight;
end = axisBounds.bottomRight;
break;
}
canvas.drawLine(
points: [start, end],
dashPattern: axisLineStyle.dashPattern,
fill: axisLineStyle.color,
stroke: axisLineStyle.color,
strokeWidthPx: axisLineStyle.strokeWidth.toDouble(),
);
}
@override
void draw(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast}) {}
@override
ViewMeasuredSizes measureHorizontallyDrawnTicks(
List<Tick> ticks, int maxWidth, int maxHeight) {
return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
}
@override
ViewMeasuredSizes measureVerticallyDrawnTicks(
List<Tick> ticks, int maxWidth, int maxHeight) {
return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
}
}

View File

@@ -0,0 +1,168 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:meta/meta.dart' show immutable, required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/line_style.dart' show LineStyle;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../spec/axis_spec.dart'
show TextStyleSpec, LineStyleSpec, TickLabelAnchor, TickLabelJustification;
import '../tick.dart' show Tick;
import 'base_tick_draw_strategy.dart' show BaseRenderSpec, BaseTickDrawStrategy;
import 'tick_draw_strategy.dart' show TickDrawStrategy;
///
@immutable
class SmallTickRendererSpec<D> extends BaseRenderSpec<D> {
final LineStyleSpec lineStyle;
final int tickLengthPx;
const SmallTickRendererSpec({
TextStyleSpec labelStyle,
this.lineStyle,
LineStyleSpec axisLineStyle,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
this.tickLengthPx,
int minimumPaddingBetweenLabelsPx,
}) : super(
labelStyle: labelStyle,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx,
axisLineStyle: axisLineStyle);
@override
TickDrawStrategy<D> createDrawStrategy(
ChartContext context, GraphicsFactory graphicsFactory) =>
new SmallTickDrawStrategy<D>(context, graphicsFactory,
tickLengthPx: tickLengthPx,
lineStyleSpec: lineStyle,
labelStyleSpec: labelStyle,
axisLineStyleSpec: axisLineStyle,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx);
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is SmallTickRendererSpec &&
lineStyle == other.lineStyle &&
tickLengthPx == other.tickLengthPx &&
super == (other));
}
@override
int get hashCode {
int hashcode = lineStyle?.hashCode ?? 0;
hashcode = (hashcode * 37) + tickLengthPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + super.hashCode;
return hashcode;
}
}
/// Draws small tick lines for each tick. Extends [BaseTickDrawStrategy].
class SmallTickDrawStrategy<D> extends BaseTickDrawStrategy<D> {
int tickLength;
LineStyle lineStyle;
SmallTickDrawStrategy(
ChartContext chartContext,
GraphicsFactory graphicsFactory, {
int tickLengthPx,
LineStyleSpec lineStyleSpec,
TextStyleSpec labelStyleSpec,
LineStyleSpec axisLineStyleSpec,
TickLabelAnchor labelAnchor,
TickLabelJustification labelJustification,
int labelOffsetFromAxisPx,
int labelOffsetFromTickPx,
int minimumPaddingBetweenLabelsPx,
}) : super(chartContext, graphicsFactory,
labelStyleSpec: labelStyleSpec,
axisLineStyleSpec: axisLineStyleSpec ?? lineStyleSpec,
labelAnchor: labelAnchor,
labelJustification: labelJustification,
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
labelOffsetFromTickPx: labelOffsetFromTickPx,
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx) {
this.tickLength = tickLengthPx ?? StyleFactory.style.tickLength;
lineStyle =
StyleFactory.style.createTickLineStyle(graphicsFactory, lineStyleSpec);
}
@override
void draw(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast}) {
Point<num> tickStart;
Point<num> tickEnd;
switch (orientation) {
case AxisOrientation.top:
double x = tick.locationPx;
tickStart = new Point(x, axisBounds.bottom - tickLength);
tickEnd = new Point(x, axisBounds.bottom);
break;
case AxisOrientation.bottom:
double x = tick.locationPx;
tickStart = new Point(x, axisBounds.top);
tickEnd = new Point(x, axisBounds.top + tickLength);
break;
case AxisOrientation.right:
double y = tick.locationPx;
tickStart = new Point(axisBounds.left, y);
tickEnd = new Point(axisBounds.left + tickLength, y);
break;
case AxisOrientation.left:
double y = tick.locationPx;
tickStart = new Point(axisBounds.right - tickLength, y);
tickEnd = new Point(axisBounds.right, y);
break;
}
canvas.drawLine(
points: [tickStart, tickEnd],
dashPattern: lineStyle.dashPattern,
fill: lineStyle.color,
stroke: lineStyle.color,
strokeWidthPx: lineStyle.strokeWidth.toDouble(),
);
drawLabel(canvas, tick,
orientation: orientation,
axisBounds: axisBounds,
drawAreaBounds: drawAreaBounds,
isFirst: isFirst,
isLast: isLast);
}
}

View File

@@ -0,0 +1,59 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:meta/meta.dart' show required;
import '../../../common/chart_canvas.dart' show ChartCanvas;
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
import '../axis.dart' show AxisOrientation;
import '../collision_report.dart' show CollisionReport;
import '../tick.dart' show Tick;
/// Strategy for drawing ticks and checking for collisions.
abstract class TickDrawStrategy<D> {
/// Decorate the existing list of ticks.
///
/// This can be used to further modify ticks after they have been generated
/// with location data and formatted labels.
void decorateTicks(List<Tick<D>> ticks);
/// Returns a [CollisionReport] indicating if there are any collisions.
CollisionReport collides(List<Tick<D>> ticks, AxisOrientation orientation);
/// Returns measurement of ticks drawn vertically.
ViewMeasuredSizes measureVerticallyDrawnTicks(
List<Tick<D>> ticks, int maxWidth, int maxHeight);
/// Returns measurement of ticks drawn horizontally.
ViewMeasuredSizes measureHorizontallyDrawnTicks(
List<Tick<D>> ticks, int maxWidth, int maxHeight);
/// Draws tick onto [ChartCanvas].
///
/// [orientation] the orientation of the axis that this [tick] belongs to.
/// [axisBounds] the bounds of the axis.
/// [drawAreaBounds] the bounds of the chart draw area adjacent to the axis.
void draw(ChartCanvas canvas, Tick<D> tick,
{@required AxisOrientation orientation,
@required Rectangle<int> axisBounds,
@required Rectangle<int> drawAreaBounds,
@required bool isFirst,
@required bool isLast});
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
Rectangle<int> axisBounds);
}

View File

@@ -0,0 +1,111 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'numeric_scale.dart' show NumericScale;
import 'ordinal_scale.dart' show OrdinalScale;
import 'scale.dart' show MutableScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
import 'tick_provider.dart' show BaseTickProvider, TickHint;
import 'time/date_time_scale.dart' show DateTimeScale;
/// Tick provider that provides ticks at the two end points of the axis range.
class EndPointsTickProvider<D> extends BaseTickProvider<D> {
@override
List<Tick<D>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required MutableScale<D> scale,
@required TickFormatter<D> formatter,
@required Map<D, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<D> tickHint,
}) {
final ticks = <Tick<D>>[];
// Check to see if the axis has been configured with some domain values.
//
// An un-configured axis has no domain step size, and its scale defaults to
// infinity.
if (scale.domainStepSize.abs() != double.infinity) {
final start = _getStartValue(tickHint, scale);
final end = _getEndValue(tickHint, scale);
final labels = formatter.format([start, end], formatterValueCache,
stepSize: scale.domainStepSize);
ticks.add(new Tick(
value: start,
textElement: graphicsFactory.createTextElement(labels[0]),
locationPx: scale[start]));
ticks.add(new Tick(
value: end,
textElement: graphicsFactory.createTextElement(labels[1]),
locationPx: scale[end]));
// Allow draw strategy to decorate the ticks.
tickDrawStrategy.decorateTicks(ticks);
}
return ticks;
}
/// Get the start value from the scale.
D _getStartValue(TickHint<D> tickHint, MutableScale<D> scale) {
Object start;
if (tickHint != null) {
start = tickHint.start;
} else {
if (scale is NumericScale) {
start = (scale as NumericScale).viewportDomain.min;
} else if (scale is DateTimeScale) {
start = (scale as DateTimeScale).viewportDomain.start;
} else if (scale is OrdinalScale) {
start = (scale as OrdinalScale).domain.first;
}
}
return start;
}
/// Get the end value from the scale.
D _getEndValue(TickHint<D> tickHint, MutableScale<D> scale) {
Object end;
if (tickHint != null) {
end = tickHint.end;
} else {
if (scale is NumericScale) {
end = (scale as NumericScale).viewportDomain.max;
} else if (scale is DateTimeScale) {
end = (scale as DateTimeScale).viewportDomain.end;
} else if (scale is OrdinalScale) {
end = (scale as OrdinalScale).domain.last;
}
}
return end;
}
}

View File

@@ -0,0 +1,76 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../axis.dart' show NumericAxis;
import 'bucketing_numeric_tick_provider.dart' show BucketingNumericTickProvider;
/// A numeric [Axis] that positions all values beneath a certain [threshold]
/// into a reserved space on the axis range. The label for the bucket line will
/// be drawn in the middle of the bucket range, rather than aligned with the
/// gridline for that value's position on the scale.
///
/// An example illustration of a bucketing measure axis on a point chart
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
/// of the axis, since they are less than the [threshold] value of 10%.
///
/// 100% ┠─────────────────────────
/// ┃ *
/// ┃ *
/// 50% ┠──────*──────────────────
/// ┃
/// ┠─────────────────────────
/// < 10% ┃ * *
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
/// 0 50 100
///
/// This axis will format numbers as percents by default.
class BucketingNumericAxis extends NumericAxis {
/// All values smaller than the threshold will be bucketed into the same
/// position in the reserved space on the axis.
num _threshold;
/// Whether or not measure values bucketed below the [threshold] should be
/// visible on the chart, or collapsed.
///
/// If this is false, then any data with measure values smaller than
/// [threshold] will be rendered at the baseline of the chart. The
bool _showBucket;
BucketingNumericAxis()
: super(tickProvider: new BucketingNumericTickProvider());
set threshold(num threshold) {
_threshold = threshold;
(tickProvider as BucketingNumericTickProvider).threshold = threshold;
}
set showBucket(bool showBucket) {
_showBucket = showBucket;
(tickProvider as BucketingNumericTickProvider).showBucket = showBucket;
}
/// Gets the location of [domain] on the axis, repositioning any value less
/// than [threshold] to the middle of the reserved bucket.
@override
double getLocation(num domain) {
if (domain == null) {
return null;
} else if (_threshold != null && domain < _threshold) {
return _showBucket ? scale[_threshold / 2] : scale[0.0];
} else {
return scale[domain];
}
}
}

View File

@@ -0,0 +1,151 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import '../numeric_scale.dart' show NumericScale;
import '../numeric_tick_provider.dart' show NumericTickProvider;
import '../tick.dart' show Tick;
import '../tick_formatter.dart' show SimpleTickFormatterBase, TickFormatter;
import '../tick_provider.dart' show TickHint;
/// Tick provider that generates ticks for a [BucketingNumericAxis].
///
/// An example illustration of a bucketing measure axis on a point chart
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
/// of the axis, since they are less than the [threshold] value of 10%.
///
/// 100% ┠─────────────────────────
/// ┃ *
/// ┃ *
/// 50% ┠──────*──────────────────
/// ┃
/// ┠─────────────────────────
/// < 10% ┃ * *
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
/// 0 50 100
///
/// This tick provider will generate ticks using the same strategy as
/// [NumericTickProvider], except that any ticks that are smaller than
/// [threshold] will be hidden with an empty label. A special tick will be added
/// at the [threshold] position, with a label offset that moves its label down
/// to the middle of the bucket.
class BucketingNumericTickProvider extends NumericTickProvider {
/// All values smaller than the threshold will be bucketed into the same
/// position in the reserved space on the axis.
num _threshold;
set threshold(num threshold) {
_threshold = threshold;
}
/// Whether or not measure values bucketed below the [threshold] should be
/// visible on the chart, or collapsed.
bool _showBucket;
set showBucket(bool showBucket) {
_showBucket = showBucket;
}
@override
List<Tick<num>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required NumericScale scale,
@required TickFormatter<num> formatter,
@required Map<num, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<num> tickHint,
}) {
if (_threshold == null) {
throw ('Bucketing threshold must be set before getting ticks.');
}
if (_showBucket == null) {
throw ('The showBucket flag must be set before getting ticks.');
}
final localFormatter = new _BucketingFormatter()
..threshold = _threshold
..originalFormatter = formatter;
final ticks = super.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: localFormatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
orientation: orientation,
viewportExtensionEnabled: viewportExtensionEnabled);
assert(scale != null);
// Create a tick for the threshold.
final thresholdTick = new Tick<num>(
value: _threshold,
textElement: graphicsFactory
.createTextElement(localFormatter.formatValue(_threshold)),
locationPx: _showBucket ? scale[_threshold] : scale[0],
labelOffsetPx:
_showBucket ? -0.5 * (scale[_threshold] - scale[0]) : 0.0);
tickDrawStrategy.decorateTicks(<Tick<num>>[thresholdTick]);
// Filter out ticks that sit below the threshold.
ticks.removeWhere((Tick<num> tick) =>
tick.value <= thresholdTick.value && tick.value != 0.0);
// Finally, add our threshold tick to the list.
ticks.add(thresholdTick);
// Make sure they are sorted by increasing value.
ticks.sort((a, b) {
if (a.value < b.value) {
return -1;
} else if (a.value > b.value) {
return 1;
} else {
return 0;
}
});
return ticks;
}
}
class _BucketingFormatter extends SimpleTickFormatterBase<num> {
/// All values smaller than the threshold will be formatted into an empty
/// string.
num threshold;
SimpleTickFormatterBase<num> originalFormatter;
/// Formats a single tick value.
String formatValue(num value) {
if (value < threshold) {
return '';
} else if (value == threshold) {
return '< ' + originalFormatter.formatValue(value);
} else {
return originalFormatter.formatValue(value);
}
}
}

View File

@@ -0,0 +1,246 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../numeric_extents.dart' show NumericExtents;
import '../numeric_scale.dart' show NumericScale;
import '../scale.dart' show RangeBandConfig, ScaleOutputExtent, StepSizeConfig;
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
import 'linear_scale_function.dart' show LinearScaleFunction;
import 'linear_scale_viewport.dart' show LinearScaleViewportSettings;
/// [NumericScale] that lays out the domain linearly across the range.
///
/// A [Scale] which converts numeric domain units to a given numeric range units
/// linearly (as opposed to other methods like log scales). This is used to map
/// the domain's values to the available pixel range of the chart using the
/// apply method.
///
/// <p>The domain extent of the scale are determined by adding all domain
/// values to the scale. It can, however, be overwritten by calling
/// [domainOverride] to define the extent of the data.
///
/// <p>The scale can be zoomed & panned by calling either [setViewportSettings]
/// with a zoom and translate, or by setting [viewportExtent] with the domain
/// extent to show in the output range.
///
/// <p>[rangeBandConfig]: By default, this scale will map the domain extent
/// exactly to the output range in a simple ratio mapping. If a
/// [RangeBandConfig] other than NONE is used to define the width of bar groups,
/// then the scale calculation may be altered to that there is a half a stepSize
/// at the start and end of the range to ensure that a bar group can be shown
/// and centered on the scale's result.
///
/// <p>[stepSizeConfig]: By default, this scale will calculate the stepSize as
/// being auto detected using the minimal distance between two consecutive
/// datum. If you don't assign a [RangeBandConfig], then changing the
/// [stepSizeConfig] is a no-op.
class LinearScale implements NumericScale {
final LinearScaleDomainInfo _domainInfo;
final LinearScaleViewportSettings _viewportSettings;
final LinearScaleFunction _scaleFunction = new LinearScaleFunction();
RangeBandConfig rangeBandConfig = const RangeBandConfig.none();
StepSizeConfig stepSizeConfig = const StepSizeConfig.auto();
bool _scaleReady = false;
LinearScale()
: _domainInfo = new LinearScaleDomainInfo(),
_viewportSettings = new LinearScaleViewportSettings();
LinearScale._copy(LinearScale other)
: _domainInfo = new LinearScaleDomainInfo.copy(other._domainInfo),
_viewportSettings =
new LinearScaleViewportSettings.copy(other._viewportSettings),
rangeBandConfig = other.rangeBandConfig,
stepSizeConfig = other.stepSizeConfig;
@override
LinearScale copy() => new LinearScale._copy(this);
//
// Domain methods
//
@override
addDomain(num domainValue) {
_domainInfo.addDomainValue(domainValue);
}
@override
resetDomain() {
_scaleReady = false;
_domainInfo.reset();
}
@override
resetViewportSettings() {
_viewportSettings.reset();
}
@override
NumericExtents get dataExtent => new NumericExtents(
_domainInfo.dataDomainStart, _domainInfo.dataDomainEnd);
@override
num get minimumDomainStep => _domainInfo.minimumDetectedDomainStep;
@override
bool canTranslate(_) => true;
@override
set domainOverride(NumericExtents domainMaxExtent) {
_domainInfo.domainOverride = domainMaxExtent;
}
get domainOverride => _domainInfo.domainOverride;
@override
int compareDomainValueToViewport(num domainValue) {
NumericExtents dataExtent = _viewportSettings.domainExtent != null
? _viewportSettings.domainExtent
: _domainInfo.extent;
return dataExtent.compareValue(domainValue);
}
//
// Viewport methods
//
@override
setViewportSettings(double viewportScale, double viewportTranslatePx) {
_viewportSettings
..scalingFactor = viewportScale
..translatePx = viewportTranslatePx
..domainExtent = null;
_scaleReady = false;
}
@override
double get viewportScalingFactor => _viewportSettings.scalingFactor;
@override
double get viewportTranslatePx => _viewportSettings.translatePx;
@override
set viewportDomain(NumericExtents extent) {
_scaleReady = false;
_viewportSettings.domainExtent = extent;
}
@override
NumericExtents get viewportDomain {
_configureScale();
return _viewportSettings.domainExtent;
}
@override
set keepViewportWithinData(bool autoAdjustViewportToNiceValues) {
_scaleReady = false;
_viewportSettings.keepViewportWithinData = true;
}
@override
bool get keepViewportWithinData => _viewportSettings.keepViewportWithinData;
@override
double computeViewportScaleFactor(double domainWindow) =>
_domainInfo.domainDiff / domainWindow;
@override
set range(ScaleOutputExtent extent) {
_viewportSettings.range = extent;
_scaleReady = false;
}
@override
ScaleOutputExtent get range => _viewportSettings.range;
//
// Scale application methods
//
@override
num operator [](num domainValue) {
_configureScale();
return _scaleFunction[domainValue];
}
@override
num reverse(double viewPixels) {
_configureScale();
final num domain = _scaleFunction.reverse(viewPixels);
return domain;
}
@override
double get rangeBand {
_configureScale();
return _scaleFunction.rangeBandPixels;
}
@override
double get stepSize {
_configureScale();
return _scaleFunction.stepSizePixels;
}
@override
double get domainStepSize => _domainInfo.minimumDetectedDomainStep.toDouble();
@override
int get rangeWidth => (range.end - range.start).abs().toInt();
@override
bool isRangeValueWithinViewport(double rangeValue) =>
range.containsValue(rangeValue);
//
// Private update
//
_configureScale() {
if (_scaleReady) return;
assert(_viewportSettings.range != null);
// If the viewport's domainExtent are set, then we can calculate the
// viewport's scaleFactor now that the domainInfo has been loaded.
// The viewport also has a chance to correct the scaleFactor.
_viewportSettings.updateViewportScaleFactor(_domainInfo);
// Now that the viewport's scalingFactor is setup, set it on the scale
// function.
_scaleFunction.updateScaleFactor(
_viewportSettings, _domainInfo, rangeBandConfig, stepSizeConfig);
// If the viewport's domainExtent are set, then we can calculate the
// viewport's translate now that the scaleFactor has been loaded.
// The viewport also has a chance to correct the translate.
_viewportSettings.updateViewportTranslatePx(
_domainInfo, _scaleFunction.scalingFactor);
// Now that the viewport has a chance to update the translate, set it on the
// scale function.
_scaleFunction.updateTranslateAndRangeBand(
_viewportSettings, _domainInfo, rangeBandConfig);
// Now that the viewport's scaleFactor and translate have been updated
// set the effective domainExtent of the viewport.
_viewportSettings.updateViewportDomainExtent(
_domainInfo, _scaleFunction.scalingFactor);
// Cached computed values are updated.
_scaleReady = true;
}
}

View File

@@ -0,0 +1,118 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../numeric_extents.dart' show NumericExtents;
/// Encapsulation of all the domain processing logic for the [LinearScale].
class LinearScaleDomainInfo {
/// User (or axis) overridden extent in domain units.
NumericExtents domainOverride;
/// The minimum added domain value.
num _dataDomainStart = double.infinity;
num get dataDomainStart => _dataDomainStart;
/// The maximum added domain value.
num _dataDomainEnd = double.negativeInfinity;
num get dataDomainEnd => _dataDomainEnd;
/// Previous domain added so we can calculate minimumDetectedDomainStep.
num _previouslyAddedDomain;
/// The step size between data points in domain units.
///
/// Measured as the minimum distance between consecutive added points.
num _minimumDetectedDomainStep = double.infinity;
num get minimumDetectedDomainStep => _minimumDetectedDomainStep;
///The diff of the nicedDomain extent.
num get domainDiff => extent.width;
LinearScaleDomainInfo();
LinearScaleDomainInfo.copy(LinearScaleDomainInfo other) {
if (other.domainOverride != null) {
domainOverride = other.domainOverride;
}
_dataDomainStart = other._dataDomainStart;
_dataDomainEnd = other._dataDomainEnd;
_previouslyAddedDomain = other._previouslyAddedDomain;
_minimumDetectedDomainStep = other._minimumDetectedDomainStep;
}
/// Resets everything back to initial state.
void reset() {
_previouslyAddedDomain = null;
_dataDomainStart = double.infinity;
_dataDomainEnd = double.negativeInfinity;
_minimumDetectedDomainStep = double.infinity;
}
/// Updates the domain extent and detected step size given the [domainValue].
void addDomainValue(num domainValue) {
if (domainValue == null || !domainValue.isFinite) {
return;
}
extendDomain(domainValue);
if (_previouslyAddedDomain != null) {
final domainStep = (domainValue - _previouslyAddedDomain).abs();
if (domainStep != 0.0 && domainStep < minimumDetectedDomainStep) {
_minimumDetectedDomainStep = domainStep;
}
}
_previouslyAddedDomain = domainValue;
}
/// Extends the data domain extent without modifying step size detection.
///
/// Returns whether the the domain interval was extended. If the domain value
/// was already contained in the domain interval, the domain interval does not
/// change.
bool extendDomain(num domainValue) {
if (domainValue == null || !domainValue.isFinite) {
return false;
}
bool domainExtended = false;
if (domainValue < _dataDomainStart) {
_dataDomainStart = domainValue;
domainExtended = true;
}
if (domainValue > _dataDomainEnd) {
_dataDomainEnd = domainValue;
domainExtended = true;
}
return domainExtended;
}
/// Returns the extent based on the current domain range and overrides.
NumericExtents get extent {
num tmpDomainStart;
num tmpDomainEnd;
if (domainOverride != null) {
// override was set.
tmpDomainStart = domainOverride.min;
tmpDomainEnd = domainOverride.max;
} else {
// domainEnd is less than domainStart if no domain values have been set.
tmpDomainStart = _dataDomainStart.isFinite ? _dataDomainStart : 0.0;
tmpDomainEnd = _dataDomainEnd.isFinite ? _dataDomainEnd : 1.0;
}
return new NumericExtents(tmpDomainStart, tmpDomainEnd);
}
}

View File

@@ -0,0 +1,201 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../scale.dart'
show RangeBandConfig, RangeBandType, StepSizeConfig, StepSizeType;
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
import 'linear_scale_viewport.dart' show LinearScaleViewportSettings;
/// Component of the LinearScale which actually handles the apply and reverse
/// function of the scale.
class LinearScaleFunction {
/// Cached rangeBand width in pixels given the RangeBandConfig and the current
/// domain & range.
double rangeBandPixels = 0.0;
/// Cached amount in domain units to shift the input value as a part of
/// translation.
num domainTranslate = 0.0;
/// Cached translation ratio for scale translation.
double scalingFactor = 1.0;
/// Cached amount in pixel units to shift the output value as a part of
/// translation.
double rangeTranslate = 0.0;
/// The calculated step size given the step size config.
double stepSizePixels = 0.0;
/// Translates the given domainValue to the range output.
double operator [](num domainValue) {
return (((domainValue + domainTranslate) * scalingFactor) + rangeTranslate)
.toDouble();
}
/// Translates the given range output back to a domainValue.
double reverse(double viewPixels) {
return ((viewPixels - rangeTranslate) / scalingFactor) - domainTranslate;
}
/// Update the scale function's scaleFactor given the current state of the
/// viewport.
void updateScaleFactor(
LinearScaleViewportSettings viewportSettings,
LinearScaleDomainInfo domainInfo,
RangeBandConfig rangeBandConfig,
StepSizeConfig stepSizeConfig) {
double rangeDiff = viewportSettings.range.diff.toDouble();
// Note: if you provided a nicing function that extends the domain, we won't
// muck with the extended side.
bool hasHalfStepAtStart =
domainInfo.extent.min == domainInfo.dataDomainStart;
bool hasHalfStepAtEnd = domainInfo.extent.max == domainInfo.dataDomainEnd;
// Determine the stepSize and reserved range values.
// The percentage of the step reserved from the scale's range due to the
// possible half step at the start and end.
double reservedRangePercentOfStep =
getStepReservationPercent(hasHalfStepAtStart, hasHalfStepAtEnd);
_updateStepSizeAndScaleFactor(viewportSettings, domainInfo, rangeDiff,
reservedRangePercentOfStep, rangeBandConfig, stepSizeConfig);
}
/// Returns the percentage of the step reserved from the output range due to
/// maybe having to hold half stepSizes on the start and end of the output.
double getStepReservationPercent(
bool hasHalfStepAtStart, bool hasHalfStepAtEnd) {
if (!hasHalfStepAtStart && !hasHalfStepAtEnd) {
return 0.0;
}
if (hasHalfStepAtStart && hasHalfStepAtEnd) {
return 1.0;
}
return 0.5;
}
/// Updates the scale function's translate and rangeBand given the current
/// state of the viewport.
void updateTranslateAndRangeBand(LinearScaleViewportSettings viewportSettings,
LinearScaleDomainInfo domainInfo, RangeBandConfig rangeBandConfig) {
// Assign the rangeTranslate using the current viewportSettings.translatePx
// and diffs.
if (domainInfo.domainDiff == 0) {
// Translate it to the center of the range.
rangeTranslate =
viewportSettings.range.start + (viewportSettings.range.diff / 2);
} else {
bool hasHalfStepAtStart =
domainInfo.extent.min == domainInfo.dataDomainStart;
// The pixel shift of the scale function due to the half a step at the
// beginning.
double reservedRangePixelShift =
hasHalfStepAtStart ? (stepSizePixels / 2.0) : 0.0;
rangeTranslate = (viewportSettings.range.start +
viewportSettings.translatePx +
reservedRangePixelShift);
}
// We need to subtract the start from any incoming domain to apply the
// scale, so flip its sign.
domainTranslate = -1 * domainInfo.extent.min;
// Update the rangeBand size.
rangeBandPixels = _calculateRangeBandSize(rangeBandConfig);
}
/// Calculates and stores the current rangeBand given the config and current
/// step size.
double _calculateRangeBandSize(RangeBandConfig rangeBandConfig) {
switch (rangeBandConfig.type) {
case RangeBandType.fixedDomain:
return rangeBandConfig.size * scalingFactor;
case RangeBandType.fixedPixel:
return rangeBandConfig.size;
case RangeBandType.fixedPixelSpaceFromStep:
return stepSizePixels - rangeBandConfig.size;
case RangeBandType.styleAssignedPercentOfStep:
case RangeBandType.fixedPercentOfStep:
return stepSizePixels * rangeBandConfig.size;
case RangeBandType.none:
return 0.0;
}
return 0.0;
}
/// Calculates and Stores the current step size and scale factor together,
/// given the viewport, domain, and config.
///
/// <p>Scale factor and step size are related closely and should be calculated
/// together so that we do not lose accuracy due to double arithmetic.
void _updateStepSizeAndScaleFactor(
LinearScaleViewportSettings viewportSettings,
LinearScaleDomainInfo domainInfo,
double rangeDiff,
double reservedRangePercentOfStep,
RangeBandConfig rangeBandConfig,
StepSizeConfig stepSizeConfig) {
final domainDiff = domainInfo.domainDiff;
// If we are going to have any rangeBands, then ensure that we account for
// needed space on the beginning and end of the range.
if (rangeBandConfig.type != RangeBandType.none) {
switch (stepSizeConfig.type) {
case StepSizeType.autoDetect:
double minimumDetectedDomainStep =
domainInfo.minimumDetectedDomainStep.toDouble();
if (minimumDetectedDomainStep != null &&
minimumDetectedDomainStep.isFinite) {
scalingFactor = viewportSettings.scalingFactor *
(rangeDiff /
(domainDiff +
(minimumDetectedDomainStep *
reservedRangePercentOfStep)));
stepSizePixels = (minimumDetectedDomainStep * scalingFactor);
} else {
stepSizePixels = rangeDiff.abs();
scalingFactor = 1.0;
}
return;
case StepSizeType.fixedPixels:
stepSizePixels = stepSizeConfig.size;
double reservedRangeForStepPixels =
stepSizePixels * reservedRangePercentOfStep;
scalingFactor = domainDiff == 0
? 1.0
: viewportSettings.scalingFactor *
(rangeDiff - reservedRangeForStepPixels) /
domainDiff;
return;
case StepSizeType.fixedDomain:
double domainStepWidth = stepSizeConfig.size;
double totalDomainDiff =
(domainDiff + (domainStepWidth * reservedRangePercentOfStep));
scalingFactor = totalDomainDiff == 0
? 1.0
: viewportSettings.scalingFactor * (rangeDiff / totalDomainDiff);
stepSizePixels = domainStepWidth * scalingFactor;
return;
}
}
// If no cases matched, use zero step size.
stepSizePixels = 0.0;
scalingFactor = domainDiff == 0
? 1.0
: viewportSettings.scalingFactor * rangeDiff / domainDiff;
}
}

View File

@@ -0,0 +1,141 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' as math show max, min;
import '../numeric_extents.dart' show NumericExtents;
import '../scale.dart' show ScaleOutputExtent;
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
/// Component of the LinearScale responsible for the configuration and
/// calculations of the viewport.
class LinearScaleViewportSettings {
/// Output extent for the scale, typically set by the axis as the pixel
/// output.
ScaleOutputExtent range;
/// Determines whether the scale should be extended to the nice values
/// provided by the tick provider. If true, we wont touch the viewport config
/// since the axis will configure it, if false, we will still ensure sane zoom
/// and translates.
bool keepViewportWithinData = true;
/// User configured viewport scale as a zoom multiplier where 1.0 is
/// 100% (default) and 2.0 is 200% zooming in making the data take up twice
/// the space (showing half as much data in the viewport).
double scalingFactor = 1.0;
/// User configured viewport translate in pixel units.
double translatePx = 0.0;
/// The current extent of the viewport in domain units.
NumericExtents _domainExtent;
set domainExtent(NumericExtents extent) {
_domainExtent = extent;
_manualDomainExtent = extent != null;
}
NumericExtents get domainExtent => _domainExtent;
/// Indicates that the viewportExtends are to be read from to determine the
/// internal scaleFactor and rangeTranslate.
bool _manualDomainExtent = false;
LinearScaleViewportSettings();
LinearScaleViewportSettings.copy(LinearScaleViewportSettings other) {
range = other.range;
keepViewportWithinData = other.keepViewportWithinData;
scalingFactor = other.scalingFactor;
translatePx = other.translatePx;
_manualDomainExtent = other._manualDomainExtent;
_domainExtent = other._domainExtent;
}
/// Resets the viewport calculated fields back to their initial settings.
void reset() {
// Likely an auto assigned viewport (niced), so reset it between draws.
scalingFactor = 1.0;
translatePx = 0.0;
domainExtent = null;
}
int get rangeWidth => range.diff.abs().toInt();
bool isRangeValueWithinViewport(double rangeValue) =>
range.containsValue(rangeValue);
/// Updates the viewport's internal scalingFactor given the current
/// domainInfo.
void updateViewportScaleFactor(LinearScaleDomainInfo domainInfo) {
// If we are loading from the viewport, then update the scalingFactor given
// the viewport size compared to the data size.
if (_manualDomainExtent) {
double viewportDomainDiff = _domainExtent?.width?.toDouble();
if (domainInfo.domainDiff != 0.0) {
scalingFactor = domainInfo.domainDiff / viewportDomainDiff;
} else {
scalingFactor = 1.0;
// The domain claims to have no date, extend it to the viewport's
domainInfo.extendDomain(_domainExtent?.min);
domainInfo.extendDomain(_domainExtent?.max);
}
}
// Make sure that the viewportSettings.scalingFactor is sane if desired.
if (!keepViewportWithinData) {
// Make sure we don't zoom out beyond the max domain extent.
scalingFactor = math.max(1.0, scalingFactor);
}
}
/// Updates the viewport's internal translate given the current domainInfo and
/// main scalingFactor from LinearScaleFunction (not internal scalingFactor).
void updateViewportTranslatePx(
LinearScaleDomainInfo domainInfo, double scaleScalingFactor) {
// If we are loading from the viewport, then update the translate now that
// the scaleFactor has been setup.
if (_manualDomainExtent) {
translatePx = (-1.0 *
scaleScalingFactor *
(_domainExtent.min - domainInfo.extent.min));
}
// Make sure that the viewportSettings.translatePx is sane if desired.
if (!keepViewportWithinData) {
int rangeDiff = range.diff.toInt();
// Make sure we don't translate beyond the max domain extent.
translatePx = math.min(0.0, translatePx);
translatePx = math.max(rangeDiff * (1.0 - scalingFactor), translatePx);
}
}
/// Calculates and stores the viewport's domainExtent if we did not load from
/// them in the first place.
void updateViewportDomainExtent(
LinearScaleDomainInfo domainInfo, double scaleScalingFactor) {
// If we didn't load from the viewport extent, then update them given the
// current scale configuration.
if (!_manualDomainExtent) {
double viewportDomainDiff = domainInfo.domainDiff / scalingFactor;
double viewportStart =
(-1.0 * translatePx / scaleScalingFactor) + domainInfo.extent.min;
_domainExtent =
new NumericExtents(viewportStart, viewportStart + viewportDomainDiff);
}
}
}

View File

@@ -0,0 +1,105 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'scale.dart' show Extents;
/// Represents the starting and ending extent of a dataset.
class NumericExtents implements Extents<num> {
final num min;
final num max;
/// Precondition: [min] <= [max].
// TODO: When initializer list asserts are supported everywhere,
// add the precondition as an initializer list assert. This is supported in
// Flutter only.
const NumericExtents(this.min, this.max);
/// Returns [Extents] based on the min and max of the given values.
/// Returns [NumericExtents.empty] if [values] are empty
factory NumericExtents.fromValues(Iterable<num> values) {
if (values.isEmpty) {
return NumericExtents.empty;
}
var min = values.first;
var max = values.first;
for (final value in values) {
if (value < min) {
min = value;
} else if (max < value) {
max = value;
}
}
return new NumericExtents(min, max);
}
/// Returns the union of this and other.
NumericExtents plus(NumericExtents other) {
if (min <= other.min) {
if (max >= other.max) {
return this;
} else {
return new NumericExtents(min, other.max);
}
} else {
if (other.max >= max) {
return other;
} else {
return new NumericExtents(other.min, max);
}
}
}
/// Compares the given [value] against the extents.
///
/// Returns -1 if the value is less than the extents.
/// Returns 0 if the value is within the extents inclusive.
/// Returns 1 if the value is greater than the extents.
int compareValue(num value) {
if (value < min) {
return -1;
}
if (value > max) {
return 1;
}
return 0;
}
bool _containsValue(double value) => compareValue(value) == 0;
// Returns true if these [NumericExtents] collides with [other].
bool overlaps(NumericExtents other) {
return _containsValue(other.min) ||
_containsValue(other.max) ||
other._containsValue(min) ||
other._containsValue(max);
}
@override
bool operator ==(other) {
return other is NumericExtents && min == other.min && max == other.max;
}
@override
int get hashCode => (min.hashCode + (max.hashCode * 31));
num get width => max - min;
@override
String toString() => 'Extent($min, $max)';
static const NumericExtents unbounded =
const NumericExtents(double.negativeInfinity, double.infinity);
static const NumericExtents empty = const NumericExtents(0.0, 0.0);
}

View File

@@ -0,0 +1,57 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'numeric_extents.dart' show NumericExtents;
import 'scale.dart' show MutableScale;
/// Scale used to convert numeric domain input units to output range units.
///
/// The input represents a continuous numeric domain which maps to a given range
/// output. This is used to map the domain's values to the available pixel
/// range of the chart.
abstract class NumericScale extends MutableScale<num> {
/// Keeps the scale and translate sane if true (default).
///
/// Setting this to false disables some pan/zoom protections that prevent you
/// from going beyond the data extent.
bool get keepViewportWithinData;
set keepViewportWithinData(bool keep);
/// Returns the extent of the actual data (not the viewport max).
NumericExtents get dataExtent;
/// Returns the minimum step size of the actual data.
num get minimumDomainStep;
/// Overrides the domain extent if set, null otherwise.
///
/// Overrides the extent of the actual data to lie about the range of the
/// data so that panning has a start and end point to go between beyond the
/// received data. This allows lazy loading of data into the gaps in the
/// expanded lied about areas.
NumericExtents get domainOverride;
set domainOverride(NumericExtents extent);
/// Returns the domain extent visible in the viewport of the drawArea.
NumericExtents get viewportDomain;
/// Sets the domain extent visible in the viewport of the drawArea.
///
/// Invalidates the viewportScale & viewportTranslatePx.
set viewportDomain(NumericExtents extent);
/// Returns the viewportScaleFactor needed to present the given domainWindow.
double computeViewportScaleFactor(double domainWindow);
}

View File

@@ -0,0 +1,585 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show log, log10e, max, min, pow;
import 'package:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import '../../common/unitconverter/identity_converter.dart'
show IdentityConverter;
import '../../common/unitconverter/unit_converter.dart' show UnitConverter;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'numeric_extents.dart' show NumericExtents;
import 'numeric_scale.dart' show NumericScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
import 'tick_provider.dart' show BaseTickProvider, TickHint;
/// Tick provider that allows you to specify how many ticks to present while
/// also choosing tick values that appear "nice" or "rounded" to the user. By
/// default it will try to guess an appropriate number of ticks given the size
/// of the range available, but the min and max tick counts can be set by
/// calling setTickCounts().
///
/// You can control whether the axis is bound to zero (default) or follows the
/// data by calling setZeroBound().
///
/// This provider will choose "nice" ticks with the following priority order.
/// * Ticks do not collide with each other.
/// * Alternate rendering is not used to avoid collisions.
/// * Provide the least amount of domain range covering all data points (while
/// still selecting "nice" ticks values.
class NumericTickProvider extends BaseTickProvider<num> {
/// Used to determine the automatic tick count calculation.
static const MIN_DIPS_BETWEEN_TICKS = 25;
/// Potential steps available to the baseTen value of the data.
static const DEFAULT_STEPS = const [
0.01,
0.02,
0.025,
0.03,
0.04,
0.05,
0.06,
0.07,
0.08,
0.09,
0.1,
0.2,
0.25,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
0.9,
1.0,
2.0,
2.50,
3.0,
4.0,
5.0,
6.0,
7.0,
8.0,
9.0
];
// Settings
/// Sets whether the the tick provider should always include a zero tick.
///
/// If set the data range may be extended to include zero.
///
/// Note that the zero value in axis units is chosen, which may be different
/// than zero value in data units if a data to axis unit converter is set.
bool zeroBound = true;
/// If your data can only be in whole numbers, then set this to true.
///
/// It should prevent the scale from choosing fractional ticks. For example,
/// if you had a office head count, don't generate a tick for 1.5, instead
/// jump to 2.
///
/// Note that the provider will choose whole number ticks in the axis units,
/// not data units if a data to axis unit converter is set.
bool dataIsInWholeNumbers = true;
// Desired min and max tick counts are set by [setFixedTickCount] and
// [setTickCount]. These are not guaranteed tick counts.
int _desiredMaxTickCount;
int _desiredMinTickCount;
/// Allowed steps the tick provider can choose from.
var _allowedSteps = DEFAULT_STEPS;
/// Convert input data units to the desired units on the axis.
/// If not set no conversion will take place.
///
/// Combining this with an appropriate [TickFormatter] would result in axis
/// ticks that are in different unit than the actual data units.
UnitConverter<num, num> dataToAxisUnitConverter =
const IdentityConverter<num>();
// Tick calculation state
num _low;
num _high;
int _rangeWidth;
int _minTickCount;
int _maxTickCount;
// The parameters used in previous tick calculation
num _prevLow;
num _prevHigh;
int _prevRangeWidth;
int _prevMinTickCount;
int _prevMaxTickCount;
bool _prevDataIsInWholeNumbers;
/// Sets the desired tick count.
///
/// While the provider will try to satisfy the requirement, it is not
/// guaranteed, such as cases where ticks may overlap or are insufficient.
///
/// [tickCount] the fixed number of major (labeled) ticks to draw for the axis
/// Passing null will result in falling back on the automatic tick count
/// assignment.
void setFixedTickCount(int tickCount) {
// Don't allow a single tick, it doesn't make sense. so tickCount > 1
_desiredMinTickCount =
tickCount != null && tickCount > 1 ? tickCount : null;
_desiredMaxTickCount = _desiredMinTickCount;
}
/// Sets the desired min and max tick count when providing ticks.
///
/// The values are suggested requirements but are not guaranteed to be the
/// actual tick count in cases where it is not possible.
///
/// [maxTickCount] The max tick count must be greater than 1.
/// [minTickCount] The min tick count must be greater than 1.
void setTickCount(int maxTickCount, int minTickCount) {
// Don't allow a single tick, it doesn't make sense. so tickCount > 1
if (maxTickCount != null && maxTickCount > 1) {
_desiredMaxTickCount = maxTickCount;
if (minTickCount != null &&
minTickCount > 1 &&
minTickCount <= _desiredMaxTickCount) {
_desiredMinTickCount = minTickCount;
} else {
_desiredMinTickCount = 2;
}
} else {
_desiredMaxTickCount = null;
_desiredMinTickCount = null;
}
}
/// Sets the allowed step sizes this tick provider can choose from.
///
/// All ticks will be a power of 10 multiple of the given step sizes.
///
/// Note that if only very few step sizes are allowed the tick range maybe
/// much bigger than the data range.
///
/// The step sizes setup here apply in axis units, which is different than
/// input units if a data to axis unit converter is set.
///
/// [steps] allowed step sizes in the [1, 10) range.
set allowedSteps(List<double> steps) {
assert(steps != null && steps.isNotEmpty);
steps.sort();
final stepSet = new Set.from(steps);
_allowedSteps = new List<double>(stepSet.length * 3);
int stepIndex = 0;
for (double step in stepSet) {
assert(1.0 <= step && step < 10.0);
_allowedSteps[stepIndex] = _removeRoundingErrors(step / 100);
_allowedSteps[stepSet.length + stepIndex] =
_removeRoundingErrors(step / 10).toDouble();
_allowedSteps[2 * stepSet.length + stepIndex] =
_removeRoundingErrors(step);
stepIndex++;
}
}
List<Tick<num>> _getTicksFromHint({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required NumericScale scale,
@required TickFormatter<num> formatter,
@required Map<num, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required TickHint<num> tickHint,
}) {
final stepSize = (tickHint.end - tickHint.start) / (tickHint.tickCount - 1);
// Find the first tick that is greater than or equal to the min
// viewportDomain.
final tickZeroShift = tickHint.start -
(stepSize *
(tickHint.start >= 0
? (tickHint.start / stepSize).floor()
: (tickHint.start / stepSize).ceil()));
final tickStart =
(scale.viewportDomain.min / stepSize).ceil() * stepSize + tickZeroShift;
final stepInfo = new _TickStepInfo(stepSize.abs(), tickStart);
final tickValues = _getTickValues(stepInfo, tickHint.tickCount);
// Create ticks from domain values.
return createTicks(tickValues,
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
stepSize: stepInfo.stepSize);
}
@override
List<Tick<num>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required NumericScale scale,
@required TickFormatter<num> formatter,
@required Map<num, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<num> tickHint,
}) {
List<Tick<num>> ticks;
_rangeWidth = scale.rangeWidth;
_updateDomainExtents(scale.viewportDomain);
// Bypass searching for a tick range since we are getting ticks using
// information in [tickHint].
if (tickHint != null) {
return _getTicksFromHint(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
tickHint: tickHint,
);
}
if (_hasTickParametersChanged() || ticks == null) {
var selectedTicksRange = double.maxFinite;
var foundPreferredTicks = false;
var viewportDomain = scale.viewportDomain;
final axisUnitsHigh = dataToAxisUnitConverter.convert(_high);
final axisUnitsLow = dataToAxisUnitConverter.convert(_low);
_updateTickCounts(axisUnitsHigh, axisUnitsLow);
// Only create a copy of the scale if [viewportExtensionEnabled].
NumericScale mutableScale =
viewportExtensionEnabled ? scale.copy() : null;
// Walk to available tick count from max to min looking for the first one
// that gives you the least amount of range used. If a non colliding tick
// count is not found use the min tick count to generate ticks.
for (int tickCount = _maxTickCount;
tickCount >= _minTickCount;
tickCount--) {
final stepInfo =
_getStepsForTickCount(tickCount, axisUnitsHigh, axisUnitsLow);
if (stepInfo == null) {
continue;
}
final firstTick = dataToAxisUnitConverter.invert(stepInfo.tickStart);
final lastTick = dataToAxisUnitConverter
.invert(stepInfo.tickStart + stepInfo.stepSize * (tickCount - 1));
final range = lastTick - firstTick;
// Calculate ticks if it is a better range or if preferred ticks have
// not been found yet.
if (range < selectedTicksRange || !foundPreferredTicks) {
final tickValues = _getTickValues(stepInfo, tickCount);
if (viewportExtensionEnabled) {
mutableScale.viewportDomain =
new NumericExtents(firstTick, lastTick);
}
// Create ticks from domain values.
final preferredTicks = createTicks(tickValues,
context: context,
graphicsFactory: graphicsFactory,
scale: viewportExtensionEnabled ? mutableScale : scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
stepSize: stepInfo.stepSize);
// Request collision check from draw strategy.
final collisionReport =
tickDrawStrategy.collides(preferredTicks, orientation);
// Don't choose colliding ticks unless it was our last resort
if (collisionReport.ticksCollide && tickCount > _minTickCount) {
continue;
}
// Only choose alternate ticks if preferred ticks is not found.
if (foundPreferredTicks && collisionReport.alternateTicksUsed) {
continue;
}
ticks = collisionReport.alternateTicksUsed
? collisionReport.ticks
: preferredTicks;
foundPreferredTicks = !collisionReport.alternateTicksUsed;
selectedTicksRange = range;
// If viewport extended, save the viewport used.
viewportDomain = mutableScale?.viewportDomain ?? scale.viewportDomain;
}
}
_setPreviousTickCalculationParameters();
// If [viewportExtensionEnabled] and has changed, then set the scale's
// viewport to what was used to generate ticks. By only setting viewport
// when it has changed, we do not trigger the flag to recalculate scale.
if (viewportExtensionEnabled && scale.viewportDomain != viewportDomain) {
scale.viewportDomain = viewportDomain;
}
}
return ticks;
}
/// Checks whether the parameters that are used in determining the right set
/// of ticks changed from the last time we calculated ticks. If not we should
/// be able to use the cached ticks.
bool _hasTickParametersChanged() {
return _low != _prevLow ||
_high != _prevHigh ||
_rangeWidth != _prevRangeWidth ||
_minTickCount != _prevMinTickCount ||
_maxTickCount != _prevMaxTickCount ||
dataIsInWholeNumbers != _prevDataIsInWholeNumbers;
}
/// Save the last set of parameters used while determining ticks.
void _setPreviousTickCalculationParameters() {
_prevLow = _low;
_prevHigh = _high;
_prevRangeWidth = _rangeWidth;
_prevMinTickCount = _minTickCount;
_prevMaxTickCount = _maxTickCount;
_prevDataIsInWholeNumbers = dataIsInWholeNumbers;
}
/// Calculates the domain extents that this provider will cover based on the
/// axis extents passed in and the settings in the numeric tick provider.
/// Stores the domain extents in [_low] and [_high].
void _updateDomainExtents(NumericExtents axisExtents) {
_low = axisExtents.min;
_high = axisExtents.max;
// Correct the extents for zero bound
if (zeroBound) {
_low = _low > 0.0 ? 0.0 : _low;
_high = _high < 0.0 ? 0.0 : _high;
}
// Correct cases where high and low equal to give the tick provider an
// actual range to go off of when picking ticks.
if (_high == _low) {
if (_high == 0.0) {
// Corner case: the only values we've seen are zero, so lets just say
// the high is 1 and leave the low at zero.
_high = 1.0;
} else {
// The values are all the same, so assume a range of -5% to +5% from the
// single value.
if (_high > 0.0) {
_high = _high * 1.05;
_low = _low * 0.95;
} else {
// (high == low) < 0
_high = _high * 0.95;
_low = _low * 1.05;
}
}
}
}
/// Given [tickCount] and the domain range, finds the smallest tick increment,
/// chosen from power of 10 multiples of allowed steps, that covers the whole
/// data range.
_TickStepInfo _getStepsForTickCount(int tickCount, num high, num low) {
// A region is the space between ticks.
final regionCount = tickCount - 1;
// If the range contains zero, ensure that zero is a tick.
if (high >= 0 && low <= 0) {
// determine the ratio of regions that are above the zero axis.
final posRegionRatio = (high > 0 ? min(1.0, high / (high - low)) : 0.0);
var positiveRegionCount = (regionCount * posRegionRatio).ceil();
var negativeRegionCount = regionCount - positiveRegionCount;
// Ensure that negative regions are not excluded, unless there are no
// regions to spare.
if (negativeRegionCount == 0 && low < 0 && regionCount > 1) {
positiveRegionCount--;
negativeRegionCount++;
}
// If we have positive and negative values, ensure that we have ticks in
// both regions.
//
// This should not happen unless the axis is manually configured with a
// tick count. [_updateTickCounts] should ensure that we have do not try
// to generate fewer than three.
assert(
!(low < 0 &&
high > 0 &&
(negativeRegionCount == 0 || positiveRegionCount == 0)),
'Numeric tick provider cannot generate ${tickCount} '
'ticks when the axis range contains both positive and negative '
'values. A minimum of three ticks are required to include zero.');
// Determine the "favored" axis direction (the one which will control the
// ticks based on having a greater value / regions).
//
// Example: 13 / 3 (4.33 per tick) vs -5 / 1 (5 per tick)
// making -5 the favored number. A step size that includes this number
// ensures the other is also includes in the opposite direction.
final favorPositive = (high > 0 ? high / positiveRegionCount : 0).abs() >
(low < 0 ? low / negativeRegionCount : 0).abs();
final favoredNum = (favorPositive ? high : low).abs();
final favoredRegionCount =
favorPositive ? positiveRegionCount : negativeRegionCount;
final favoredTensBase = (_getEnclosingPowerOfTen(favoredNum)).abs();
// Check each step size and see if it would contain the "favored" value
for (double step in _allowedSteps) {
final tmpStepSize = _removeRoundingErrors(step * favoredTensBase);
// If prefer whole number, then don't allow a step that isn't one.
if (dataIsInWholeNumbers && (tmpStepSize).round() != tmpStepSize) {
continue;
}
// TODO: Skip steps that format to the same string.
// But wait until the last step to prevent the cost of the formatter.
// Potentially store the formatted strings in TickStepInfo?
if (tmpStepSize * favoredRegionCount >= favoredNum) {
double stepStart = negativeRegionCount > 0
? (-1 * tmpStepSize * negativeRegionCount)
: 0.0;
return new _TickStepInfo(tmpStepSize, stepStart);
}
}
} else {
// Find the range base to calculate step sizes.
final diffTensBase = _getEnclosingPowerOfTen(high - low);
// Walk the step sizes calculating a starting point and seeing if the high
// end is included in the range given that step size.
for (double step in _allowedSteps) {
final tmpStepSize = _removeRoundingErrors(step * diffTensBase);
// If prefer whole number, then don't allow a step that isn't one.
if (dataIsInWholeNumbers && (tmpStepSize).round() != tmpStepSize) {
continue;
}
// TODO: Skip steps that format to the same string.
// But wait until the last step to prevent the cost of the formatter.
double tmpStepStart = _getStepLessThan(low, tmpStepSize);
if (tmpStepStart + (tmpStepSize * regionCount) >= high) {
return new _TickStepInfo(tmpStepSize, tmpStepStart);
}
}
}
return new _TickStepInfo(1.0, low.floorToDouble());
}
List<double> _getTickValues(_TickStepInfo steps, int tickCount) {
final tickValues = new List<double>(tickCount);
// We have our size and start, assign all the tick values to the given array.
for (int i = 0; i < tickCount; i++) {
tickValues[i] = dataToAxisUnitConverter.invert(
_removeRoundingErrors(steps.tickStart + (i * steps.stepSize)));
}
return tickValues;
}
/// Given the axisDimensions update the tick counts given they are not fixed.
void _updateTickCounts(num high, num low) {
int tmpMaxNumMajorTicks;
int tmpMinNumMajorTicks;
// If the domain range contains both positive and negative values, then we
// need a minimum of three ticks to include zero as a tick. Otherwise, we
// only need an upper and lower tick.
final absoluteMinTicks = (low < 0 && 0 < high) ? 3 : 2;
// If there is a desired tick range use it, if not calculate one.
if (_desiredMaxTickCount != null) {
tmpMinNumMajorTicks = max(_desiredMinTickCount, absoluteMinTicks);
tmpMaxNumMajorTicks = max(_desiredMaxTickCount, tmpMinNumMajorTicks);
} else {
double minPixelsPerTick = MIN_DIPS_BETWEEN_TICKS.toDouble();
tmpMinNumMajorTicks = absoluteMinTicks;
tmpMaxNumMajorTicks =
max(absoluteMinTicks, (_rangeWidth / minPixelsPerTick).floor());
}
// Don't blow away the previous array if it hasn't changed.
if (tmpMaxNumMajorTicks != _maxTickCount ||
tmpMinNumMajorTicks != _minTickCount) {
_maxTickCount = tmpMaxNumMajorTicks;
_minTickCount = tmpMinNumMajorTicks;
}
}
/// Returns the power of 10 which contains the [number].
///
/// If [number] is 0 returns 1.
/// Examples:
/// [number] of 63 returns 100
/// [number] of -63 returns -100
/// [number] of 0.63 returns 1
static double _getEnclosingPowerOfTen(num number) {
if (number == 0) {
return 1.0;
}
return pow(10, (log10e * log(number.abs())).ceil()) *
(number < 0.0 ? -1.0 : 1.0);
}
/// Returns the step numerically less than the number by step increments.
static double _getStepLessThan(double number, double stepSize) {
if (number == 0.0 || stepSize == 0.0) {
return 0.0;
}
return (stepSize > 0.0
? (number / stepSize).floor()
: (number / stepSize).ceil()) *
stepSize;
}
/// Attempts to slice off very small floating point rounding effects for the
/// given number.
///
/// @param number the number to round.
/// @return the rounded number.
static double _removeRoundingErrors(double number) {
// sufficiently large multiplier to handle generating ticks on the order
// of 10^-9.
const multiplier = 1.0e9;
return number > 100.0
? number.roundToDouble()
: (number * multiplier).roundToDouble() / multiplier;
}
}
class _TickStepInfo {
double stepSize;
double tickStart;
_TickStepInfo(this.stepSize, this.tickStart);
}

View File

@@ -0,0 +1,44 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show HashSet;
import 'scale.dart' show Extents;
/// A range of ordinals.
class OrdinalExtents extends Extents<String> {
final List<String> _range;
/// The extents representing the ordinal values in [range].
///
/// The elements of [range] must all be unique.
///
/// [D] is the domain class type for the elements in the extents.
OrdinalExtents(List<String> range) : _range = range {
// This asserts that all elements in [range] are unique.
final uniqueValueCount = new HashSet.from(_range).length;
assert(uniqueValueCount == range.length);
}
factory OrdinalExtents.all(List<String> range) => new OrdinalExtents(range);
bool get isEmpty => _range.isEmpty;
/// The number of values inside this extent.
int get length => _range.length;
String operator [](int index) => _range[index];
int indexOf(String value) => _range.indexOf(value);
}

View File

@@ -0,0 +1,40 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'ordinal_scale_domain_info.dart' show OrdinalScaleDomainInfo;
import 'scale.dart' show MutableScale;
abstract class OrdinalScale extends MutableScale<String> {
/// The current domain collection with all added unique values.
OrdinalScaleDomainInfo get domain;
/// Sets the viewport of the scale based on the number of data points to show
/// and the starting domain value.
///
/// [viewportDataSize] How many ordinal domain values to show in the viewport.
/// [startingDomain] The starting domain value of the viewport. Note that if
/// the starting domain is in terms of position less than [domainValuesToShow]
/// from the last domain value the viewport will be fixed to the last value
/// and not guaranteed that this domain value is the first in the viewport.
void setViewport(int viewportDataSize, String startingDomain);
/// The number of full ordinal steps that fit in the viewport.
int get viewportDataSize;
/// The first fully visible ordinal step within the viewport.
///
/// Null if no domains exist.
String get viewportStartingDomain;
}

View File

@@ -0,0 +1,77 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show HashMap;
import 'ordinal_extents.dart' show OrdinalExtents;
/// A domain processor for [OrdinalScale].
///
/// [D] domain class type of the values being tracked.
///
/// Unique domain values are kept, so duplicates will not increase the extent.
class OrdinalScaleDomainInfo {
int _index = 0;
/// A map of domain value and the order it was added.
final _domainsToOrder = new HashMap<String, int>();
/// A list of domain values kept to support [getDomainAtIndex].
final _domainList = <String>[];
OrdinalScaleDomainInfo();
OrdinalScaleDomainInfo copy() {
return new OrdinalScaleDomainInfo()
.._domainsToOrder.addAll(_domainsToOrder)
.._index = _index
.._domainList.addAll(_domainList);
}
void add(String domain) {
if (!_domainsToOrder.containsKey(domain)) {
_domainsToOrder[domain] = _index;
_index += 1;
_domainList.add(domain);
}
}
int indexOf(String domain) => _domainsToOrder[domain];
String getDomainAtIndex(int index) {
assert(index >= 0);
assert(index < _index);
return _domainList[index];
}
List<String> get domains => _domainList;
String get first => _domainList.isEmpty ? null : _domainList.first;
String get last => _domainList.isEmpty ? null : _domainList.last;
bool get isEmpty => (_index == 0);
bool get isNotEmpty => !isEmpty;
OrdinalExtents get extent => new OrdinalExtents.all(_domainList);
int get size => _index;
/// Clears all domain values.
void clear() {
_domainsToOrder.clear();
_domainList.clear();
_index = 0;
}
}

View File

@@ -0,0 +1,58 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'ordinal_scale.dart' show OrdinalScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
import 'tick_provider.dart' show BaseTickProvider, TickHint;
/// A strategy for selecting ticks to draw given ordinal domain values.
class OrdinalTickProvider extends BaseTickProvider<String> {
const OrdinalTickProvider();
@override
List<Tick<String>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required List<String> domainValues,
@required OrdinalScale scale,
@required TickFormatter formatter,
@required Map<String, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<String> tickHint,
}) {
return createTicks(scale.domain.domains,
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy);
}
@override
bool operator ==(other) => other is OrdinalTickProvider;
@override
int get hashCode => 31;
}

View File

@@ -0,0 +1,313 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' as math show max, min;
/// Scale used to convert data input domain units to output range units.
///
/// This is the immutable portion of the Scale definition. Used for converting
/// data from the dataset in domain units to an output in range units (likely
/// pixel range of the area to draw on).
///
/// <p>The Scale/MutableScale split is to show the intention of what you can or
/// should be doing with the scale during different stages of chart draw
/// process.
///
/// [D] is the domain class type for the values passed in.
abstract class Scale<D> {
/// Applies the scale function to the [domainValue].
///
/// Returns the pixel location for the given [domainValue] or null if the
/// domainValue could not be found/translated by this scale.
/// Non-numeric scales should be the only ones that can return null.
num operator [](D domainValue);
/// Reverse application of the scale.
D reverse(double pixelLocation);
/// Tests a [domainValue] to see if the scale can translate it.
///
/// Returns true if the scale can translate the given domainValue.
/// (Ex: linear scales can translate any number, but ordinal scales can only
/// translate values previously passed in.)
bool canTranslate(D domainValue);
/// Returns the previously set output range for the scale function.
ScaleOutputExtent get range;
/// Returns the absolute width between the max and min range values.
int get rangeWidth;
/// Returns the configuration used to determine the rangeBand.
///
/// This is most often used to define the bar group width.
RangeBandConfig get rangeBandConfig;
/// Returns the rangeBand width in pixels.
///
/// The rangeBand is determined using the RangeBandConfig potentially with the
/// measured step size. This value is used as the bar group width. If
/// StepSizeConfig is set to auto detect, then you must wait until after
/// the chart's onPostLayout phase before you'll get a valid number.
double get rangeBand;
/// Returns the stepSize width in pixels.
///
/// The step size is determined using the [StepSizeConfig].
double get stepSize;
/// Returns the stepSize domain value.
double get domainStepSize;
/// Tests whether the given [domainValue] is within the axis' range.
///
/// Returns < 0 if the [domainValue] would plot before the viewport, 0 if it
/// would plot within the viewport and > 0 if it would plot beyond the
/// viewport of the axis.
int compareDomainValueToViewport(D domainValue);
/// Returns true if the given [rangeValue] point is within the output range.
///
/// Not to be confused with the start and end of the domain.
bool isRangeValueWithinViewport(double rangeValue);
/// Returns the current viewport scale.
///
/// A scale of 1.0 would map the data directly to the output range, while a
/// value of 2.0 would map the data to an output of double the range so you
/// only see half the data in the viewport. This is the equivalent to
/// zooming. Its value is likely >= 1.0.
double get viewportScalingFactor;
/// Returns the current pixel viewport offset
///
/// The translate is used by the scale function when it applies the scale.
/// This is the equivalent to panning. Its value is likely <= 0 to pan the
/// data to the left.
double get viewportTranslatePx;
/// Returns a mutable copy of the scale.
///
/// Mutating the returned scale will not effect the original one.
MutableScale<D> copy();
}
/// Mutable extension of the [Scale] definition.
///
/// Used for converting data from the dataset to some range (likely pixel range)
/// of the area to draw on.
///
/// [D] the domain class type for the values passed in.
abstract class MutableScale<D> extends Scale<D> {
/// Reset the domain for this [Scale].
void resetDomain();
/// Reset the viewport settings for this [Scale].
void resetViewportSettings();
/// Add [domainValue] to this [Scale]'s domain.
///
/// Domains should be added in order to allow proper stepSize detection.
/// [domainValue] is the data value to add to the scale used to update the
/// domain extent.
void addDomain(D domainValue);
/// Sets the output range to use for the scale's conversion.
///
/// The range start is mapped to the domain's min and the range end is
/// mapped to the domain's max for the conversion using the domain nicing
/// function.
///
/// [extent] is the extent of the range which will likely be the pixel
/// range of the drawing area to convert to.
set range(ScaleOutputExtent extent);
/// Configures the zoom and translate.
///
/// [viewportScale] is the zoom factor to use, likely >= 1.0 where 1.0 maps
/// the complete data extents to the output range, and 2.0 only maps half the
/// data to the output range.
///
/// [viewportTranslatePx] is the translate/pan to use in pixel units,
/// likely <= 0 which shifts the start of the data before the edge of the
/// chart giving us a pan.
void setViewportSettings(double viewportScale, double viewportTranslatePx);
/// Sets the configuration used to determine the rangeBand (bar group width).
set rangeBandConfig(RangeBandConfig barGroupWidthConfig);
/// Sets the method for determining the step size.
///
/// This is the domain space between data points.
StepSizeConfig get stepSizeConfig;
set stepSizeConfig(StepSizeConfig config);
}
/// Tuple of the output for a scale in pixels from [start] to [end] inclusive.
///
/// It is different from [Extent] because it focuses on start and end and not
/// min and max, meaning that start could be greater or less than end.
class ScaleOutputExtent {
final int start;
final int end;
const ScaleOutputExtent(this.start, this.end);
int get min => math.min(start, end);
int get max => math.max(start, end);
bool containsValue(double value) => value >= min && value <= max;
/// Returns the difference between the extents.
///
/// If the [end] is less than the [start] (think vertical measure axis), then
/// this will correctly return a negative value.
int get diff => end - start;
/// Returns the width of the extent.
int get width => diff.abs();
@override
bool operator ==(other) =>
other is ScaleOutputExtent && start == other.start && end == other.end;
@override
int get hashCode => start.hashCode + (end.hashCode * 31);
@override
String toString() => "ScaleOutputRange($start, $end)";
}
/// Type of RangeBand used to determine the rangeBand size units.
enum RangeBandType {
/// No rangeBand (not suitable for bars or step line charts).
none,
/// Size is specified in pixel units.
fixedPixel,
/// Size is specified domain scale units.
fixedDomain,
/// Size is a percentage of the minimum step size between points.
fixedPercentOfStep,
/// Size is a style pack assigned percentage of the minimum step size between
/// points.
styleAssignedPercentOfStep,
/// Size is subtracted from the minimum step size between points in pixel
/// units.
fixedPixelSpaceFromStep,
}
/// Defines the method for calculating the rangeBand of the Scale.
///
/// The rangeBand is used to determine the width of a group of bars. The term
/// rangeBand comes from the d3 JavaScript library which the JS library uses
/// internally.
///
/// <p>RangeBandConfig is immutable, See factory methods for creating one.
class RangeBandConfig {
final RangeBandType type;
/// The width of the band in units specified by the bandType.
final double size;
/// Creates a rangeBand definition of zero, no rangeBand.
const RangeBandConfig.none()
: type = RangeBandType.none,
size = 0.0;
/// Creates a fixed rangeBand definition in pixel width.
///
/// Used to determine a bar width or a step width in the line renderer.
const RangeBandConfig.fixedPixel(double pixels)
: type = RangeBandType.fixedPixel,
size = pixels;
/// Creates a fixed rangeBand definition in domain unit width.
///
/// Used to determine a bar width or a step width in the line renderer.
const RangeBandConfig.fixedDomain(double domainSize)
: type = RangeBandType.fixedDomain,
size = domainSize;
/// Creates a config that defines the rangeBand as equal to the stepSize.
const RangeBandConfig.stepChartBand()
: type = RangeBandType.fixedPercentOfStep,
size = 1.0;
/// Creates a config that defines the rangeBand as percentage of the stepSize.
///
/// [percentOfStepWidth] is the percentage of the step from 0.0 - 1.0.
RangeBandConfig.percentOfStep(double percentOfStepWidth)
: type = RangeBandType.fixedPercentOfStep,
size = percentOfStepWidth {
assert(percentOfStepWidth >= 0 && percentOfStepWidth <= 1.0);
}
/// Creates a config that assigns the rangeBand according to the stylepack.
///
/// <p>Note: renderers can detect this setting and update the percent based on
/// the number of series in their preprocess.
RangeBandConfig.styleAssignedPercent([int seriesCount = 1])
: type = RangeBandType.styleAssignedPercentOfStep,
// TODO: retrieve value from the stylepack once available.
size = 0.65;
/// Creates a config that defines the rangeBand as the stepSize - pixels.
///
/// Where fixedPixels() gave you a constant rangBand in pixels, this will give
/// you a constant space between rangeBands in pixels.
const RangeBandConfig.fixedPixelSpaceBetweenStep(double pixels)
: type = RangeBandType.fixedPixelSpaceFromStep,
size = pixels;
}
/// Type of step size calculation to use.
enum StepSizeType { autoDetect, fixedDomain, fixedPixels }
/// Defines the method for calculating the stepSize between points.
///
/// Typically auto will work fine in most cases, but if your data is
/// irregular or you only have one data point, then you may want to override the
/// stepSize detection specifying the exact expected stepSize.
class StepSizeConfig {
final StepSizeType type;
final double size;
/// Creates a StepSizeConfig that calculates step size based on incoming data.
///
/// The stepSize is determined is calculated by detecting the smallest
/// distance between two adjacent data points. This may not be suitable if
/// you have irregular data or just a single data point.
const StepSizeConfig.auto()
: type = StepSizeType.autoDetect,
size = 0.0;
/// Creates a StepSizeConfig specifying the exact step size in pixel units.
const StepSizeConfig.fixedPixels(double pixels)
: type = StepSizeType.fixedPixels,
size = pixels;
/// Creates a StepSizeConfig specifying the exact step size in domain units.
const StepSizeConfig.fixedDomain(double domainSize)
: type = StepSizeType.fixedDomain,
size = domainSize;
}
// TODO: make other extent subclasses plural.
abstract class Extents<D> {}

View File

@@ -0,0 +1,344 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show min, max;
import 'ordinal_scale.dart' show OrdinalScale;
import 'ordinal_scale_domain_info.dart' show OrdinalScaleDomainInfo;
import 'scale.dart'
show
RangeBandConfig,
RangeBandType,
StepSizeConfig,
StepSizeType,
ScaleOutputExtent;
/// Scale that converts ordinal values of type [D] to a given range output.
///
/// A `SimpleOrdinalScale` is used to map values from its domain to the
/// available pixel range of the chart. Typically used for bar charts where the
/// width of the bar is [rangeBand] and the position of the bar is retrieved
/// by [[]].
class SimpleOrdinalScale implements OrdinalScale {
final _stepSizeConfig = new StepSizeConfig.auto();
OrdinalScaleDomainInfo _domain;
ScaleOutputExtent _range = new ScaleOutputExtent(0, 1);
double _viewportScale = 1.0;
double _viewportTranslatePx = 0.0;
RangeBandConfig _rangeBandConfig = new RangeBandConfig.styleAssignedPercent();
bool _scaleChanged = true;
double _cachedStepSizePixels;
double _cachedRangeBandShift;
double _cachedRangeBandSize;
int _viewportDataSize;
String _viewportStartingDomain;
SimpleOrdinalScale() : _domain = new OrdinalScaleDomainInfo();
SimpleOrdinalScale._copy(SimpleOrdinalScale other)
: _domain = other._domain.copy(),
_range = new ScaleOutputExtent(other._range.start, other._range.end),
_viewportScale = other._viewportScale,
_viewportTranslatePx = other._viewportTranslatePx,
_rangeBandConfig = other._rangeBandConfig;
@override
double get rangeBand {
if (_scaleChanged) {
_updateScale();
}
return _cachedRangeBandSize;
}
@override
double get stepSize {
if (_scaleChanged) {
_updateScale();
}
return _cachedStepSizePixels;
}
@override
double get domainStepSize => 1.0;
@override
set rangeBandConfig(RangeBandConfig barGroupWidthConfig) {
if (barGroupWidthConfig == null) {
throw new ArgumentError.notNull('RangeBandConfig must not be null.');
}
if (barGroupWidthConfig.type == RangeBandType.fixedDomain ||
barGroupWidthConfig.type == RangeBandType.none) {
throw new ArgumentError(
'barGroupWidthConfig must not be NONE or FIXED_DOMAIN');
}
_rangeBandConfig = barGroupWidthConfig;
_scaleChanged = true;
}
@override
RangeBandConfig get rangeBandConfig => _rangeBandConfig;
@override
set stepSizeConfig(StepSizeConfig config) {
if (config != null && config.type != StepSizeType.autoDetect) {
throw new ArgumentError(
'Ordinal scales only support StepSizeConfig of type Auto');
}
// Nothing is set because only auto is supported.
}
@override
StepSizeConfig get stepSizeConfig => _stepSizeConfig;
/// Converts [domainValue] to the position to place the band/bar.
///
/// Returns 0 if not found.
@override
num operator [](String domainValue) {
if (_scaleChanged) {
_updateScale();
}
final i = _domain.indexOf(domainValue);
if (i != null) {
return viewportTranslatePx +
_range.start +
_cachedRangeBandShift +
(_cachedStepSizePixels * i);
}
// If it wasn't found
return 0.0;
}
@override
String reverse(double pixelLocation) {
final index = ((pixelLocation -
viewportTranslatePx -
_range.start -
_cachedRangeBandShift) /
_cachedStepSizePixels);
// The last pixel belongs in the last step even if it tries to round up.
//
// Index may be less than 0 when [pixelLocation] is less than the width of
// the range band shift. This may happen on the far left side of the chart,
// where we want the first datum anyways. Wrapping the result in "max(0, x)"
// cuts off these negative values.
return _domain
.getDomainAtIndex(max(0, min(index.round(), domain.size - 1)));
}
@override
bool canTranslate(String domainValue) =>
(_domain.indexOf(domainValue) != null);
@override
OrdinalScaleDomainInfo get domain => _domain;
/// Update the scale to include [domainValue].
@override
void addDomain(String domainValue) {
_domain.add(domainValue);
_scaleChanged = true;
}
@override
set range(ScaleOutputExtent extent) {
_range = extent;
_scaleChanged = true;
}
@override
ScaleOutputExtent get range => _range;
@override
resetDomain() {
_domain.clear();
_scaleChanged = true;
}
@override
resetViewportSettings() {
_viewportScale = 1.0;
_viewportTranslatePx = 0.0;
_scaleChanged = true;
}
@override
int get rangeWidth => (range.start - range.end).abs().toInt();
@override
double get viewportScalingFactor => _viewportScale;
@override
double get viewportTranslatePx => _viewportTranslatePx;
@override
void setViewportSettings(double viewportScale, double viewportTranslatePx) {
_viewportScale = viewportScale;
_viewportTranslatePx =
min(0.0, max(rangeWidth * (1.0 - viewportScale), viewportTranslatePx));
_scaleChanged = true;
}
@override
void setViewport(int viewportDataSize, String startingDomain) {
if (startingDomain != null &&
viewportDataSize != null &&
viewportDataSize <= 0) {
throw new ArgumentError('viewportDataSize can' 't be less than 1.');
}
_scaleChanged = true;
_viewportDataSize = viewportDataSize;
_viewportStartingDomain = startingDomain;
}
/// Update this scale's viewport using settings [_viewportDataSize] and
/// [_viewportStartingDomain].
void _updateViewport() {
setViewportSettings(1.0, 0.0);
_recalculateScale();
if (_domain.isEmpty) {
return;
}
// Update the scale with zoom level to help find the correct translate.
setViewportSettings(
_domain.size / min(_viewportDataSize, _domain.size), 0.0);
_recalculateScale();
final domainIndex = _domain.indexOf(_viewportStartingDomain);
if (domainIndex != null) {
// Update the translate so that the scale starts half a step before the
// chosen domain.
final viewportTranslatePx = -(_cachedStepSizePixels * domainIndex);
setViewportSettings(_viewportScale, viewportTranslatePx);
}
}
@override
int get viewportDataSize {
if (_scaleChanged) {
_updateScale();
}
return _domain.isEmpty ? 0 : (rangeWidth ~/ _cachedStepSizePixels);
}
@override
String get viewportStartingDomain {
if (_scaleChanged) {
_updateScale();
}
if (_domain.isEmpty) {
return null;
}
return _domain.getDomainAtIndex(
(-_viewportTranslatePx / _cachedStepSizePixels).ceil().toInt());
}
@override
bool isRangeValueWithinViewport(double rangeValue) {
return range != null && rangeValue >= range.min && rangeValue <= range.max;
}
@override
int compareDomainValueToViewport(String domainValue) {
// TODO: This currently works because range defaults to 0-1
// This needs to be looked into further.
var i = _domain.indexOf(domainValue);
if (i != null && range != null) {
var domainPx = this[domainValue];
if (domainPx < range.min) {
return -1;
}
if (domainPx > range.max) {
return 1;
}
return 0;
}
return -1;
}
@override
SimpleOrdinalScale copy() => new SimpleOrdinalScale._copy(this);
void _updateCachedFields(
double stepSizePixels, double rangeBandPixels, double rangeBandShift) {
_cachedStepSizePixels = stepSizePixels;
_cachedRangeBandSize = rangeBandPixels;
_cachedRangeBandShift = rangeBandShift;
// TODO: When there are horizontal bars increasing from where
// the domain and measure axis intersects but the desired behavior is
// flipped. The plan is to fix this by fixing code to flip the range in the
// code.
// If range start is less than range end, then the domain is calculated by
// adding the band width. If range start is greater than range end, then the
// domain is calculated by subtracting from the band width (ex. horizontal
// bar charts where first series is at the bottom of the chart).
if (range.start > range.end) {
_cachedStepSizePixels *= -1;
_cachedRangeBandShift *= -1;
}
_scaleChanged = false;
}
void _updateScale() {
if (_viewportStartingDomain != null && _viewportDataSize != null) {
// Update viewport recalculates the scale.
_updateViewport();
}
_recalculateScale();
}
void _recalculateScale() {
final stepSizePixels = _domain.isEmpty
? 0.0
: _viewportScale * (rangeWidth.toDouble() / _domain.size.toDouble());
double rangeBandPixels;
switch (rangeBandConfig.type) {
case RangeBandType.fixedPixel:
rangeBandPixels = rangeBandConfig.size.toDouble();
break;
case RangeBandType.fixedPixelSpaceFromStep:
var spaceInPixels = rangeBandConfig.size.toDouble();
rangeBandPixels = max(0.0, stepSizePixels - spaceInPixels);
break;
case RangeBandType.styleAssignedPercentOfStep:
case RangeBandType.fixedPercentOfStep:
var percent = rangeBandConfig.size.toDouble();
rangeBandPixels = stepSizePixels * percent;
break;
case RangeBandType.fixedDomain:
case RangeBandType.none:
default:
throw new StateError('RangeBandType must not be NONE or FIXED_DOMAIN');
break;
}
_updateCachedFields(stepSizePixels, rangeBandPixels, stepSizePixels / 2.0);
}
}

View File

@@ -0,0 +1,181 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show immutable;
import '../../../../common/color.dart' show Color;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show Axis;
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import '../tick_formatter.dart' show TickFormatter;
import '../tick_provider.dart' show TickProvider;
@immutable
class AxisSpec<D> {
final bool showAxisLine;
final RenderSpec<D> renderSpec;
final TickProviderSpec<D> tickProviderSpec;
final TickFormatterSpec<D> tickFormatterSpec;
const AxisSpec({
this.renderSpec,
this.tickProviderSpec,
this.tickFormatterSpec,
this.showAxisLine,
});
factory AxisSpec.from(
AxisSpec other, {
RenderSpec<D> renderSpec,
TickProviderSpec<D> tickProviderSpec,
TickFormatterSpec<D> tickFormatterSpec,
bool showAxisLine,
}) {
return new AxisSpec(
renderSpec: renderSpec ?? other.renderSpec,
tickProviderSpec: tickProviderSpec ?? other.tickProviderSpec,
tickFormatterSpec: tickFormatterSpec ?? other.tickFormatterSpec,
showAxisLine: showAxisLine ?? other.showAxisLine,
);
}
configure(
Axis<D> axis, ChartContext context, GraphicsFactory graphicsFactory) {
if (showAxisLine != null) {
axis.forceDrawAxisLine = showAxisLine;
}
if (renderSpec != null) {
axis.tickDrawStrategy =
renderSpec.createDrawStrategy(context, graphicsFactory);
}
if (tickProviderSpec != null) {
axis.tickProvider = tickProviderSpec.createTickProvider(context);
}
if (tickFormatterSpec != null) {
axis.tickFormatter = tickFormatterSpec.createTickFormatter(context);
}
}
/// Creates an appropriately typed [Axis].
Axis<D> createAxis() => null;
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AxisSpec &&
renderSpec == other.renderSpec &&
tickProviderSpec == other.tickProviderSpec &&
tickFormatterSpec == other.tickFormatterSpec &&
showAxisLine == other.showAxisLine);
@override
int get hashCode {
int hashcode = renderSpec?.hashCode ?? 0;
hashcode = (hashcode * 37) + tickProviderSpec.hashCode;
hashcode = (hashcode * 37) + tickFormatterSpec.hashCode;
hashcode = (hashcode * 37) + showAxisLine.hashCode;
return hashcode;
}
}
@immutable
abstract class TickProviderSpec<D> {
TickProvider<D> createTickProvider(ChartContext context);
}
@immutable
abstract class TickFormatterSpec<D> {
TickFormatter<D> createTickFormatter(ChartContext context);
}
@immutable
abstract class RenderSpec<D> {
const RenderSpec();
TickDrawStrategy<D> createDrawStrategy(
ChartContext context, GraphicsFactory graphicFactory);
}
@immutable
class TextStyleSpec {
final String fontFamily;
final int fontSize;
final Color color;
const TextStyleSpec({this.fontFamily, this.fontSize, this.color});
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is TextStyleSpec &&
fontFamily == other.fontFamily &&
fontSize == other.fontSize &&
color == other.color);
}
@override
int get hashCode {
int hashcode = fontFamily?.hashCode ?? 0;
hashcode = (hashcode * 37) + fontSize?.hashCode ?? 0;
hashcode = (hashcode * 37) + color?.hashCode ?? 0;
return hashcode;
}
}
@immutable
class LineStyleSpec {
final Color color;
final List<int> dashPattern;
final int thickness;
const LineStyleSpec({this.color, this.dashPattern, this.thickness});
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is LineStyleSpec &&
color == other.color &&
dashPattern == other.dashPattern &&
thickness == other.thickness);
}
@override
int get hashCode {
int hashcode = color?.hashCode ?? 0;
hashcode = (hashcode * 37) + dashPattern?.hashCode ?? 0;
hashcode = (hashcode * 37) + thickness?.hashCode ?? 0;
return hashcode;
}
}
enum TickLabelAnchor {
before,
centered,
after,
/// The top most tick draws all text under the location.
/// The bottom most tick draws all text above the location.
/// The rest of the ticks are centered.
inside,
}
enum TickLabelJustification {
inside,
outside,
}

View File

@@ -0,0 +1,170 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:intl/intl.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show Axis, NumericAxis;
import '../linear/bucketing_numeric_axis.dart' show BucketingNumericAxis;
import '../linear/bucketing_numeric_tick_provider.dart'
show BucketingNumericTickProvider;
import '../numeric_extents.dart' show NumericExtents;
import 'axis_spec.dart' show AxisSpec, RenderSpec;
import 'numeric_axis_spec.dart'
show
BasicNumericTickFormatterSpec,
BasicNumericTickProviderSpec,
NumericAxisSpec,
NumericTickProviderSpec,
NumericTickFormatterSpec;
/// A numeric [AxisSpec] that positions all values beneath a certain [threshold]
/// into a reserved space on the axis range. The label for the bucket line will
/// be drawn in the middle of the bucket range, rather than aligned with the
/// gridline for that value's position on the scale.
///
/// An example illustration of a bucketing measure axis on a point chart
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
/// of the axis, since they are less than the [threshold] value of 10%.
///
/// 100% ┠─────────────────────────
/// ┃ *
/// ┃ *
/// 50% ┠──────*──────────────────
/// ┃
/// ┠─────────────────────────
/// < 10% ┃ * *
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
/// 0 50 100
///
/// This axis will format numbers as percents by default.
@immutable
class BucketingAxisSpec extends NumericAxisSpec {
/// All values smaller than the threshold will be bucketed into the same
/// position in the reserved space on the axis.
final num threshold;
/// Whether or not measure values bucketed below the [threshold] should be
/// visible on the chart, or collapsed.
///
/// If this is false, then any data with measure values smaller than
/// [threshold] will not be rendered on the chart.
final bool showBucket;
/// Creates a [NumericAxisSpec] that is specialized for percentage data.
BucketingAxisSpec({
RenderSpec<num> renderSpec,
NumericTickProviderSpec tickProviderSpec,
NumericTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
bool showBucket,
this.threshold,
NumericExtents viewport,
}) : this.showBucket = showBucket ?? true,
super(
renderSpec: renderSpec,
tickProviderSpec:
tickProviderSpec ?? const BucketingNumericTickProviderSpec(),
tickFormatterSpec: tickFormatterSpec ??
new BasicNumericTickFormatterSpec.fromNumberFormat(
new NumberFormat.percentPattern()),
showAxisLine: showAxisLine,
viewport: viewport ?? const NumericExtents(0.0, 1.0));
@override
configure(
Axis<num> axis, ChartContext context, GraphicsFactory graphicsFactory) {
super.configure(axis, context, graphicsFactory);
if (axis is NumericAxis && viewport != null) {
axis.setScaleViewport(viewport);
}
if (axis is BucketingNumericAxis && threshold != null) {
axis.threshold = threshold;
}
if (axis is BucketingNumericAxis && showBucket != null) {
axis.showBucket = showBucket;
}
}
@override
BucketingNumericAxis createAxis() => new BucketingNumericAxis();
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is BucketingAxisSpec &&
showBucket == other.showBucket &&
threshold == other.threshold &&
super == (other));
@override
int get hashCode {
int hashcode = super.hashCode;
hashcode = (hashcode * 37) + showBucket.hashCode;
hashcode = (hashcode * 37) + threshold.hashCode;
return hashcode;
}
}
@immutable
class BucketingNumericTickProviderSpec extends BasicNumericTickProviderSpec {
/// Creates a [TickProviderSpec] that generates ticks for a bucketing axis.
///
/// [zeroBound] automatically include zero in the data range.
/// [dataIsInWholeNumbers] skip over ticks that would produce
/// fractional ticks that don't make sense for the domain (ie: headcount).
/// [desiredTickCount] the fixed number of ticks to try to make. Convenience
/// that sets [desiredMinTickCount] and [desiredMaxTickCount] the same.
/// Both min and max win out if they are set along with
/// [desiredTickCount].
/// [desiredMinTickCount] automatically choose the best tick
/// count to produce the 'nicest' ticks but make sure we have this many.
/// [desiredMaxTickCount] automatically choose the best tick
/// count to produce the 'nicest' ticks but make sure we don't have more
/// than this many.
const BucketingNumericTickProviderSpec(
{bool zeroBound,
bool dataIsInWholeNumbers,
int desiredTickCount,
int desiredMinTickCount,
int desiredMaxTickCount})
: super(
zeroBound: zeroBound ?? true,
dataIsInWholeNumbers: dataIsInWholeNumbers ?? false,
desiredTickCount: desiredTickCount,
desiredMinTickCount: desiredMinTickCount,
desiredMaxTickCount: desiredMaxTickCount,
);
@override
BucketingNumericTickProvider createTickProvider(ChartContext context) {
final provider = new BucketingNumericTickProvider()
..zeroBound = zeroBound
..dataIsInWholeNumbers = dataIsInWholeNumbers;
if (desiredMinTickCount != null ||
desiredMaxTickCount != null ||
desiredTickCount != null) {
provider.setTickCount(desiredMaxTickCount ?? desiredTickCount ?? 10,
desiredMinTickCount ?? desiredTickCount ?? 2);
}
return provider;
}
}

View File

@@ -0,0 +1,327 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show immutable;
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show Axis;
import '../end_points_tick_provider.dart' show EndPointsTickProvider;
import '../static_tick_provider.dart' show StaticTickProvider;
import '../time/auto_adjusting_date_time_tick_provider.dart'
show AutoAdjustingDateTimeTickProvider;
import '../time/date_time_axis.dart' show DateTimeAxis;
import '../time/date_time_extents.dart' show DateTimeExtents;
import '../time/date_time_tick_formatter.dart' show DateTimeTickFormatter;
import '../time/day_time_stepper.dart' show DayTimeStepper;
import '../time/hour_tick_formatter.dart' show HourTickFormatter;
import '../time/time_range_tick_provider_impl.dart'
show TimeRangeTickProviderImpl;
import '../time/time_tick_formatter.dart' show TimeTickFormatter;
import '../time/time_tick_formatter_impl.dart'
show CalendarField, TimeTickFormatterImpl;
import 'axis_spec.dart'
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
import 'tick_spec.dart' show TickSpec;
/// Generic [AxisSpec] specialized for Timeseries charts.
@immutable
class DateTimeAxisSpec extends AxisSpec<DateTime> {
/// Sets viewport for this Axis.
///
/// If pan / zoom behaviors are set, this is the initial viewport.
final DateTimeExtents viewport;
/// Creates a [AxisSpec] that specialized for timeseries charts.
///
/// [renderSpec] spec used to configure how the ticks and labels
/// actually render. Possible values are [GridlineRendererSpec],
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
/// given to the RenderSpec is of type [DateTime] for Timeseries.
/// [tickProviderSpec] spec used to configure what ticks are generated.
/// [tickFormatterSpec] spec used to configure how the tick labels
/// are formatted.
/// [showAxisLine] override to force the axis to draw the axis
/// line.
const DateTimeAxisSpec({
RenderSpec<DateTime> renderSpec,
DateTimeTickProviderSpec tickProviderSpec,
DateTimeTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
this.viewport,
}) : super(
renderSpec: renderSpec,
tickProviderSpec: tickProviderSpec,
tickFormatterSpec: tickFormatterSpec,
showAxisLine: showAxisLine);
@override
configure(Axis<DateTime> axis, ChartContext context,
GraphicsFactory graphicsFactory) {
super.configure(axis, context, graphicsFactory);
if (axis is DateTimeAxis && viewport != null) {
axis.setScaleViewport(viewport);
}
}
Axis<DateTime> createAxis() {
assert(false, 'Call createDateTimeAxis() to create a DateTimeAxis.');
return null;
}
/// Creates a [DateTimeAxis]. This should be called in place of createAxis.
DateTimeAxis createDateTimeAxis(DateTimeFactory dateTimeFactory) =>
new DateTimeAxis(dateTimeFactory);
@override
bool operator ==(Object other) =>
other is DateTimeAxisSpec &&
viewport == other.viewport &&
super == (other);
@override
int get hashCode {
int hashcode = super.hashCode;
hashcode = (hashcode * 37) + viewport.hashCode;
return hashcode;
}
}
abstract class DateTimeTickProviderSpec extends TickProviderSpec<DateTime> {}
abstract class DateTimeTickFormatterSpec extends TickFormatterSpec<DateTime> {}
/// [TickProviderSpec] that sets up the automatically assigned time ticks based
/// on the extents of your data.
@immutable
class AutoDateTimeTickProviderSpec implements DateTimeTickProviderSpec {
final bool includeTime;
/// Creates a [TickProviderSpec] that dynamically chooses ticks based on the
/// extents of the data.
///
/// [includeTime] - flag that indicates whether the time should be
/// included when choosing appropriate tick intervals.
const AutoDateTimeTickProviderSpec({this.includeTime = true});
@override
AutoAdjustingDateTimeTickProvider createTickProvider(ChartContext context) {
if (includeTime) {
return new AutoAdjustingDateTimeTickProvider.createDefault(
context.dateTimeFactory);
} else {
return new AutoAdjustingDateTimeTickProvider.createWithoutTime(
context.dateTimeFactory);
}
}
@override
bool operator ==(Object other) =>
other is AutoDateTimeTickProviderSpec && includeTime == other.includeTime;
@override
int get hashCode => includeTime?.hashCode ?? 0;
}
/// [TickProviderSpec] that sets up time ticks with days increments only.
@immutable
class DayTickProviderSpec implements DateTimeTickProviderSpec {
final List<int> increments;
const DayTickProviderSpec({this.increments});
/// Creates a [TickProviderSpec] that dynamically chooses ticks based on the
/// extents of the data, limited to day increments.
///
/// [increments] specify the number of day increments that can be chosen from
/// when searching for the appropriate tick intervals.
@override
AutoAdjustingDateTimeTickProvider createTickProvider(ChartContext context) {
return new AutoAdjustingDateTimeTickProvider.createWith([
new TimeRangeTickProviderImpl(new DayTimeStepper(context.dateTimeFactory,
allowedTickIncrements: increments))
]);
}
@override
bool operator ==(Object other) =>
other is DayTickProviderSpec && increments == other.increments;
@override
int get hashCode => increments?.hashCode ?? 0;
}
/// [TickProviderSpec] that sets up time ticks at the two end points of the axis
/// range.
@immutable
class DateTimeEndPointsTickProviderSpec implements DateTimeTickProviderSpec {
const DateTimeEndPointsTickProviderSpec();
/// Creates a [TickProviderSpec] that dynamically chooses time ticks at the
/// two end points of the axis range
@override
EndPointsTickProvider<DateTime> createTickProvider(ChartContext context) {
return new EndPointsTickProvider<DateTime>();
}
@override
bool operator ==(Object other) => other is DateTimeEndPointsTickProviderSpec;
}
/// [TickProviderSpec] that allows you to specific the ticks to be used.
@immutable
class StaticDateTimeTickProviderSpec implements DateTimeTickProviderSpec {
final List<TickSpec<DateTime>> tickSpecs;
const StaticDateTimeTickProviderSpec(this.tickSpecs);
@override
StaticTickProvider<DateTime> createTickProvider(ChartContext context) =>
new StaticTickProvider<DateTime>(tickSpecs);
@override
bool operator ==(Object other) =>
other is StaticDateTimeTickProviderSpec && tickSpecs == other.tickSpecs;
@override
int get hashCode => tickSpecs.hashCode;
}
/// Formatters for a single level of the [DateTimeTickFormatterSpec].
@immutable
class TimeFormatterSpec {
final String format;
final String transitionFormat;
final String noonFormat;
/// Creates a formatter for a particular granularity of data.
///
/// [format] [DateFormat] format string used to format non-transition ticks.
/// The string is given to the dateTimeFactory to support i18n formatting.
/// [transitionFormat] [DateFormat] format string used to format transition
/// ticks. Examples of transition ticks:
/// Day ticks would have a transition tick at month boundaries.
/// Hour ticks would have a transition tick at day boundaries.
/// The first tick is typically a transition tick.
/// [noonFormat] [DateFormat] format string used only for formatting hours
/// in the event that you want to format noon differently than other
/// hours (ie: [10, 11, 12p, 1, 2, 3]).
const TimeFormatterSpec(
{this.format, this.transitionFormat, this.noonFormat});
@override
bool operator ==(Object other) =>
other is TimeFormatterSpec &&
format == other.format &&
transitionFormat == other.transitionFormat &&
noonFormat == other.noonFormat;
@override
int get hashCode {
int hashcode = format?.hashCode ?? 0;
hashcode = (hashcode * 37) + transitionFormat?.hashCode ?? 0;
hashcode = (hashcode * 37) + noonFormat?.hashCode ?? 0;
return hashcode;
}
}
/// [TickFormatterSpec] that automatically chooses the appropriate level of
/// formatting based on the tick stepSize. Each level of date granularity has
/// its own [TimeFormatterSpec] used to specify the formatting strings at that
/// level.
@immutable
class AutoDateTimeTickFormatterSpec implements DateTimeTickFormatterSpec {
final TimeFormatterSpec minute;
final TimeFormatterSpec hour;
final TimeFormatterSpec day;
final TimeFormatterSpec month;
final TimeFormatterSpec year;
/// Creates a [TickFormatterSpec] that automatically chooses the formatting
/// given the individual [TimeFormatterSpec] formatters that are set.
///
/// There is a default formatter for each level that is configurable, but
/// by specifying a level here it replaces the default for that particular
/// granularity. This is useful for swapping out one or all of the formatters.
const AutoDateTimeTickFormatterSpec(
{this.minute, this.hour, this.day, this.month, this.year});
@override
DateTimeTickFormatter createTickFormatter(ChartContext context) {
final Map<int, TimeTickFormatter> map = {};
if (minute != null) {
map[DateTimeTickFormatter.MINUTE] =
_makeFormatter(minute, CalendarField.hourOfDay, context);
}
if (hour != null) {
map[DateTimeTickFormatter.HOUR] =
_makeFormatter(hour, CalendarField.date, context);
}
if (day != null) {
map[23 * DateTimeTickFormatter.HOUR] =
_makeFormatter(day, CalendarField.month, context);
}
if (month != null) {
map[28 * DateTimeTickFormatter.DAY] =
_makeFormatter(month, CalendarField.year, context);
}
if (year != null) {
map[364 * DateTimeTickFormatter.DAY] =
_makeFormatter(year, CalendarField.year, context);
}
return new DateTimeTickFormatter(context.dateTimeFactory, overrides: map);
}
TimeTickFormatterImpl _makeFormatter(TimeFormatterSpec spec,
CalendarField transitionField, ChartContext context) {
if (spec.noonFormat != null) {
return new HourTickFormatter(
dateTimeFactory: context.dateTimeFactory,
simpleFormat: spec.format,
transitionFormat: spec.transitionFormat,
noonFormat: spec.noonFormat);
} else {
return new TimeTickFormatterImpl(
dateTimeFactory: context.dateTimeFactory,
simpleFormat: spec.format,
transitionFormat: spec.transitionFormat,
transitionField: transitionField);
}
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AutoDateTimeTickFormatterSpec &&
minute == other.minute &&
hour == other.hour &&
day == other.day &&
month == other.month &&
year == other.year);
@override
int get hashCode {
int hashcode = minute?.hashCode ?? 0;
hashcode = (hashcode * 37) + hour?.hashCode ?? 0;
hashcode = (hashcode * 37) + day?.hashCode ?? 0;
hashcode = (hashcode * 37) + month?.hashCode ?? 0;
hashcode = (hashcode * 37) + year?.hashCode ?? 0;
return hashcode;
}
}

View File

@@ -0,0 +1,65 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show immutable;
import '../draw_strategy/small_tick_draw_strategy.dart'
show SmallTickRendererSpec;
import '../time/date_time_extents.dart' show DateTimeExtents;
import 'axis_spec.dart' show AxisSpec, RenderSpec, TickLabelAnchor;
import 'date_time_axis_spec.dart'
show
DateTimeAxisSpec,
DateTimeEndPointsTickProviderSpec,
DateTimeTickFormatterSpec,
DateTimeTickProviderSpec;
/// Default [AxisSpec] used for Timeseries charts.
@immutable
class EndPointsTimeAxisSpec extends DateTimeAxisSpec {
/// Creates a [AxisSpec] that specialized for timeseries charts.
///
/// [renderSpec] spec used to configure how the ticks and labels
/// actually render. Possible values are [GridlineRendererSpec],
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
/// given to the RenderSpec is of type [DateTime] for Timeseries.
/// [tickProviderSpec] spec used to configure what ticks are generated.
/// [tickFormatterSpec] spec used to configure how the tick labels
/// are formatted.
/// [showAxisLine] override to force the axis to draw the axis
/// line.
const EndPointsTimeAxisSpec({
RenderSpec<DateTime> renderSpec,
DateTimeTickProviderSpec tickProviderSpec,
DateTimeTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
DateTimeExtents viewport,
bool usingBarRenderer = false,
}) : super(
renderSpec: renderSpec ??
const SmallTickRendererSpec<DateTime>(
labelAnchor: TickLabelAnchor.inside,
labelOffsetFromTickPx: 0),
tickProviderSpec:
tickProviderSpec ?? const DateTimeEndPointsTickProviderSpec(),
tickFormatterSpec: tickFormatterSpec,
showAxisLine: showAxisLine,
viewport: viewport);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is EndPointsTimeAxisSpec && super == (other));
}

View File

@@ -0,0 +1,253 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart';
import 'package:meta/meta.dart' show immutable;
import 'package:intl/intl.dart';
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../../../common/datum_details.dart' show MeasureFormatter;
import '../axis.dart' show Axis, NumericAxis;
import '../end_points_tick_provider.dart' show EndPointsTickProvider;
import '../numeric_extents.dart' show NumericExtents;
import '../numeric_tick_provider.dart' show NumericTickProvider;
import '../static_tick_provider.dart' show StaticTickProvider;
import '../tick_formatter.dart' show NumericTickFormatter;
import 'axis_spec.dart'
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
import 'tick_spec.dart' show TickSpec;
/// [AxisSpec] specialized for numeric/continuous axes like the measure axis.
@immutable
class NumericAxisSpec extends AxisSpec<num> {
/// Sets viewport for this Axis.
///
/// If pan / zoom behaviors are set, this is the initial viewport.
final NumericExtents viewport;
/// Creates a [AxisSpec] that specialized for numeric data.
///
/// [renderSpec] spec used to configure how the ticks and labels
/// actually render. Possible values are [GridlineRendererSpec],
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
/// given to the RenderSpec is of type [num] when using this spec.
/// [tickProviderSpec] spec used to configure what ticks are generated.
/// [tickFormatterSpec] spec used to configure how the tick labels are
/// formatted.
/// [showAxisLine] override to force the axis to draw the axis line.
const NumericAxisSpec({
RenderSpec<num> renderSpec,
NumericTickProviderSpec tickProviderSpec,
NumericTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
this.viewport,
}) : super(
renderSpec: renderSpec,
tickProviderSpec: tickProviderSpec,
tickFormatterSpec: tickFormatterSpec,
showAxisLine: showAxisLine);
factory NumericAxisSpec.from(
NumericAxisSpec other, {
RenderSpec<num> renderSpec,
TickProviderSpec tickProviderSpec,
TickFormatterSpec tickFormatterSpec,
bool showAxisLine,
NumericExtents viewport,
}) {
return new NumericAxisSpec(
renderSpec: renderSpec ?? other.renderSpec,
tickProviderSpec: tickProviderSpec ?? other.tickProviderSpec,
tickFormatterSpec: tickFormatterSpec ?? other.tickFormatterSpec,
showAxisLine: showAxisLine ?? other.showAxisLine,
viewport: viewport ?? other.viewport,
);
}
@override
configure(
Axis<num> axis, ChartContext context, GraphicsFactory graphicsFactory) {
super.configure(axis, context, graphicsFactory);
if (axis is NumericAxis && viewport != null) {
axis.setScaleViewport(viewport);
}
}
@override
NumericAxis createAxis() => new NumericAxis();
@override
bool operator ==(Object other) =>
other is NumericAxisSpec &&
viewport == other.viewport &&
super == (other);
@override
int get hashCode {
int hashcode = super.hashCode;
hashcode = (hashcode * 37) + viewport.hashCode;
hashcode = (hashcode * 37) + super.hashCode;
return hashcode;
}
}
abstract class NumericTickProviderSpec extends TickProviderSpec<num> {}
abstract class NumericTickFormatterSpec extends TickFormatterSpec<num> {}
@immutable
class BasicNumericTickProviderSpec implements NumericTickProviderSpec {
final bool zeroBound;
final bool dataIsInWholeNumbers;
final int desiredTickCount;
final int desiredMinTickCount;
final int desiredMaxTickCount;
/// Creates a [TickProviderSpec] that dynamically chooses the number of
/// ticks based on the extents of the data.
///
/// [zeroBound] automatically include zero in the data range.
/// [dataIsInWholeNumbers] skip over ticks that would produce
/// fractional ticks that don't make sense for the domain (ie: headcount).
/// [desiredTickCount] the fixed number of ticks to try to make. Convenience
/// that sets [desiredMinTickCount] and [desiredMaxTickCount] the same.
/// Both min and max win out if they are set along with
/// [desiredTickCount].
/// [desiredMinTickCount] automatically choose the best tick
/// count to produce the 'nicest' ticks but make sure we have this many.
/// [desiredMaxTickCount] automatically choose the best tick
/// count to produce the 'nicest' ticks but make sure we don't have more
/// than this many.
const BasicNumericTickProviderSpec(
{this.zeroBound,
this.dataIsInWholeNumbers,
this.desiredTickCount,
this.desiredMinTickCount,
this.desiredMaxTickCount});
@override
NumericTickProvider createTickProvider(ChartContext context) {
final provider = new NumericTickProvider();
if (zeroBound != null) {
provider.zeroBound = zeroBound;
}
if (dataIsInWholeNumbers != null) {
provider.dataIsInWholeNumbers = dataIsInWholeNumbers;
}
if (desiredMinTickCount != null ||
desiredMaxTickCount != null ||
desiredTickCount != null) {
provider.setTickCount(desiredMaxTickCount ?? desiredTickCount ?? 10,
desiredMinTickCount ?? desiredTickCount ?? 2);
}
return provider;
}
@override
bool operator ==(Object other) =>
other is BasicNumericTickProviderSpec &&
zeroBound == other.zeroBound &&
dataIsInWholeNumbers == other.dataIsInWholeNumbers &&
desiredTickCount == other.desiredTickCount &&
desiredMinTickCount == other.desiredMinTickCount &&
desiredMaxTickCount == other.desiredMaxTickCount;
@override
int get hashCode {
int hashcode = zeroBound?.hashCode ?? 0;
hashcode = (hashcode * 37) + dataIsInWholeNumbers?.hashCode ?? 0;
hashcode = (hashcode * 37) + desiredTickCount?.hashCode ?? 0;
hashcode = (hashcode * 37) + desiredMinTickCount?.hashCode ?? 0;
hashcode = (hashcode * 37) + desiredMaxTickCount?.hashCode ?? 0;
return hashcode;
}
}
/// [TickProviderSpec] that sets up numeric ticks at the two end points of the
/// axis range.
@immutable
class NumericEndPointsTickProviderSpec implements NumericTickProviderSpec {
/// Creates a [TickProviderSpec] that dynamically chooses numeric ticks at the
/// two end points of the axis range
const NumericEndPointsTickProviderSpec();
@override
EndPointsTickProvider<num> createTickProvider(ChartContext context) {
return new EndPointsTickProvider<num>();
}
@override
bool operator ==(Object other) => other is NumericEndPointsTickProviderSpec;
}
/// [TickProviderSpec] that allows you to specific the ticks to be used.
@immutable
class StaticNumericTickProviderSpec implements NumericTickProviderSpec {
final List<TickSpec<num>> tickSpecs;
const StaticNumericTickProviderSpec(this.tickSpecs);
@override
StaticTickProvider<num> createTickProvider(ChartContext context) =>
new StaticTickProvider<num>(tickSpecs);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is StaticNumericTickProviderSpec && tickSpecs == other.tickSpecs);
@override
int get hashCode => tickSpecs.hashCode;
}
@immutable
class BasicNumericTickFormatterSpec implements NumericTickFormatterSpec {
final MeasureFormatter formatter;
final NumberFormat numberFormat;
/// Simple [TickFormatterSpec] that delegates formatting to the given
/// [NumberFormat].
const BasicNumericTickFormatterSpec(this.formatter) : numberFormat = null;
const BasicNumericTickFormatterSpec.fromNumberFormat(this.numberFormat)
: formatter = null;
/// A formatter will be created with the number format if it is not null.
/// Otherwise, it will create one with the [MeasureFormatter] callback.
@override
NumericTickFormatter createTickFormatter(ChartContext context) {
return numberFormat != null
? new NumericTickFormatter.fromNumberFormat(numberFormat)
: new NumericTickFormatter(formatter: formatter);
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is BasicNumericTickFormatterSpec &&
formatter == other.formatter &&
numberFormat == other.numberFormat);
}
@override
int get hashCode {
int hashcode = formatter.hashCode;
hashcode = (hashcode * 37) * numberFormat.hashCode;
return hashcode;
}
}

View File

@@ -0,0 +1,139 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show immutable;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show Axis, OrdinalAxis, OrdinalViewport;
import '../ordinal_tick_provider.dart' show OrdinalTickProvider;
import '../static_tick_provider.dart' show StaticTickProvider;
import '../tick_formatter.dart' show OrdinalTickFormatter;
import 'axis_spec.dart'
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
import 'tick_spec.dart' show TickSpec;
/// [AxisSpec] specialized for ordinal/non-continuous axes typically for bars.
@immutable
class OrdinalAxisSpec extends AxisSpec<String> {
/// Sets viewport for this Axis.
///
/// If pan / zoom behaviors are set, this is the initial viewport.
final OrdinalViewport viewport;
/// Creates a [AxisSpec] that specialized for ordinal domain charts.
///
/// [renderSpec] spec used to configure how the ticks and labels
/// actually render. Possible values are [GridlineRendererSpec],
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
/// given to the RenderSpec is of type [String] when using this spec.
/// [tickProviderSpec] spec used to configure what ticks are generated.
/// [tickFormatterSpec] spec used to configure how the tick labels are
/// formatted.
/// [showAxisLine] override to force the axis to draw the axis line.
const OrdinalAxisSpec({
RenderSpec<String> renderSpec,
OrdinalTickProviderSpec tickProviderSpec,
OrdinalTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
this.viewport,
}) : super(
renderSpec: renderSpec,
tickProviderSpec: tickProviderSpec,
tickFormatterSpec: tickFormatterSpec,
showAxisLine: showAxisLine);
@override
configure(Axis<String> axis, ChartContext context,
GraphicsFactory graphicsFactory) {
super.configure(axis, context, graphicsFactory);
if (axis is OrdinalAxis && viewport != null) {
axis.setScaleViewport(viewport);
}
}
@override
OrdinalAxis createAxis() => new OrdinalAxis();
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other is OrdinalAxisSpec &&
viewport == other.viewport &&
super == (other));
}
@override
int get hashCode {
int hashcode = super.hashCode;
hashcode = (hashcode * 37) + viewport.hashCode;
return hashcode;
}
}
abstract class OrdinalTickProviderSpec extends TickProviderSpec<String> {}
abstract class OrdinalTickFormatterSpec extends TickFormatterSpec<String> {}
@immutable
class BasicOrdinalTickProviderSpec implements OrdinalTickProviderSpec {
const BasicOrdinalTickProviderSpec();
@override
OrdinalTickProvider createTickProvider(ChartContext context) =>
new OrdinalTickProvider();
@override
bool operator ==(Object other) => other is BasicOrdinalTickProviderSpec;
@override
int get hashCode => 37;
}
/// [TickProviderSpec] that allows you to specific the ticks to be used.
@immutable
class StaticOrdinalTickProviderSpec implements OrdinalTickProviderSpec {
final List<TickSpec<String>> tickSpecs;
const StaticOrdinalTickProviderSpec(this.tickSpecs);
@override
StaticTickProvider<String> createTickProvider(ChartContext context) =>
new StaticTickProvider<String>(tickSpecs);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is StaticOrdinalTickProviderSpec && tickSpecs == other.tickSpecs);
@override
int get hashCode => tickSpecs.hashCode;
}
@immutable
class BasicOrdinalTickFormatterSpec implements OrdinalTickFormatterSpec {
const BasicOrdinalTickFormatterSpec();
@override
OrdinalTickFormatter createTickFormatter(ChartContext context) =>
new OrdinalTickFormatter();
@override
bool operator ==(Object other) => other is BasicOrdinalTickFormatterSpec;
@override
int get hashCode => 37;
}

View File

@@ -0,0 +1,54 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show immutable;
import 'package:intl/intl.dart';
import '../numeric_extents.dart' show NumericExtents;
import 'axis_spec.dart' show AxisSpec, RenderSpec;
import 'numeric_axis_spec.dart'
show
BasicNumericTickFormatterSpec,
BasicNumericTickProviderSpec,
NumericAxisSpec,
NumericTickProviderSpec,
NumericTickFormatterSpec;
/// Convenience [AxisSpec] specialized for numeric percentage axes.
@immutable
class PercentAxisSpec extends NumericAxisSpec {
/// Creates a [NumericAxisSpec] that is specialized for percentage data.
PercentAxisSpec({
RenderSpec<num> renderSpec,
NumericTickProviderSpec tickProviderSpec,
NumericTickFormatterSpec tickFormatterSpec,
bool showAxisLine,
NumericExtents viewport,
}) : super(
renderSpec: renderSpec,
tickProviderSpec: tickProviderSpec ??
const BasicNumericTickProviderSpec(dataIsInWholeNumbers: false),
tickFormatterSpec: tickFormatterSpec ??
new BasicNumericTickFormatterSpec.fromNumberFormat(
new NumberFormat.percentPattern()),
showAxisLine: showAxisLine,
viewport: viewport ?? const NumericExtents(0.0, 1.0));
@override
bool operator ==(Object other) =>
other is PercentAxisSpec &&
viewport == other.viewport &&
super == (other);
}

View File

@@ -0,0 +1,32 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'axis_spec.dart' show TextStyleSpec;
/// Definition for a tick.
///
/// Used to define a tick that is used by static tick provider.
class TickSpec<D> {
final D value;
final String label;
final TextStyleSpec style;
/// [value] the value of this tick
/// [label] optional label for this tick. If not set, uses the tick formatter
/// of the axis.
/// [style] optional style for this tick. If not set, uses the style of the
/// axis.
const TickSpec(this.value, {this.label, this.style});
}

View File

@@ -0,0 +1,106 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'numeric_scale.dart' show NumericScale;
import 'scale.dart' show MutableScale;
import 'spec/tick_spec.dart' show TickSpec;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
import 'tick_provider.dart' show TickProvider, TickHint;
import 'time/date_time_scale.dart' show DateTimeScale;
/// A strategy that uses the ticks provided and only assigns positioning.
///
/// The [TextStyle] is not overridden during tick draw strategy decorateTicks.
/// If it is null, then the default is used.
class StaticTickProvider<D> extends TickProvider<D> {
final List<TickSpec<D>> tickSpec;
StaticTickProvider(this.tickSpec);
@override
List<Tick<D>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required MutableScale<D> scale,
@required TickFormatter<D> formatter,
@required Map<D, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<D> tickHint,
}) {
final ticks = <Tick<D>>[];
bool allTicksHaveLabels = true;
for (TickSpec<D> spec in tickSpec) {
// When static ticks are being used with a numeric axis, extend the axis
// with the values specified.
if (scale is NumericScale || scale is DateTimeScale) {
scale.addDomain(spec.value);
}
// Save off whether all ticks have labels.
allTicksHaveLabels = allTicksHaveLabels && (spec.label != null);
}
// Use the formatter's label if the tick spec does not provide one.
List<String> formattedValues;
if (allTicksHaveLabels == false) {
formattedValues = formatter.format(
tickSpec.map((spec) => spec.value).toList(), formatterValueCache,
stepSize: scale.domainStepSize);
}
for (var i = 0; i < tickSpec.length; i++) {
final spec = tickSpec[i];
// We still check if the spec is within the viewport because we do not
// extend the axis for OrdinalScale.
if (scale.compareDomainValueToViewport(spec.value) == 0) {
final tick = new Tick<D>(
value: spec.value,
textElement: graphicsFactory
.createTextElement(spec.label ?? formattedValues[i]),
locationPx: scale[spec.value]);
if (spec.style != null) {
tick.textElement.textStyle = graphicsFactory.createTextPaint()
..fontFamily = spec.style.fontFamily
..fontSize = spec.style.fontSize
..color = spec.style.color;
}
ticks.add(tick);
}
}
// Allow draw strategy to decorate the ticks.
tickDrawStrategy.decorateTicks(ticks);
return ticks;
}
@override
bool operator ==(other) =>
other is StaticTickProvider && tickSpec == other.tickSpec;
@override
int get hashCode => tickSpec.hashCode;
}

View File

@@ -0,0 +1,47 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart';
import '../../../common/text_element.dart';
/// A labeled point on an axis.
///
/// [D] is the type of the value this tick is associated with.
class Tick<D> {
/// The value that this tick represents
final D value;
/// [TextElement] for this tick.
TextElement textElement;
/// Location on the axis where this tick is rendered (in canvas coordinates).
double locationPx;
/// Offset of the label for this tick from its location.
///
/// This is a vertical offset for ticks on a vertical axis, or horizontal
/// offset for ticks on a horizontal axis.
double labelOffsetPx;
Tick(
{@required this.value,
@required this.textElement,
this.locationPx,
this.labelOffsetPx});
@override
String toString() => 'Tick(value: $value, locationPx: $locationPx, '
'labelOffsetPx: $labelOffsetPx)';
}

View File

@@ -0,0 +1,107 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:intl/intl.dart';
import '../../common/datum_details.dart' show MeasureFormatter;
// TODO: Break out into separate files.
/// A strategy used for converting domain values of the ticks into Strings.
///
/// [D] is the domain type.
abstract class TickFormatter<D> {
const TickFormatter();
/// Formats a list of tick values.
List<String> format(List<D> tickValues, Map<D, String> cache, {num stepSize});
}
abstract class SimpleTickFormatterBase<D> implements TickFormatter<D> {
const SimpleTickFormatterBase();
@override
List<String> format(List<D> tickValues, Map<D, String> cache,
{num stepSize}) =>
tickValues.map((D value) {
// Try to use the cached formats first.
String formattedString = cache[value];
if (formattedString == null) {
formattedString = formatValue(value);
cache[value] = formattedString;
}
return formattedString;
}).toList();
/// Formats a single tick value.
String formatValue(D value);
}
/// A strategy that converts tick labels using toString().
class OrdinalTickFormatter extends SimpleTickFormatterBase<String> {
const OrdinalTickFormatter();
@override
String formatValue(String value) => value;
@override
bool operator ==(other) => other is OrdinalTickFormatter;
@override
int get hashCode => 31;
}
/// A strategy for formatting the labels on numeric ticks using [NumberFormat].
///
/// The default format is [NumberFormat.decimalPattern].
class NumericTickFormatter extends SimpleTickFormatterBase<num> {
final MeasureFormatter formatter;
NumericTickFormatter._internal(this.formatter);
/// Construct a a new [NumericTickFormatter].
///
/// [formatter] optionally specify a formatter to be used. Defaults to using
/// [NumberFormat.decimalPattern] if none is specified.
factory NumericTickFormatter({MeasureFormatter formatter}) {
formatter ??= _getFormatter(new NumberFormat.decimalPattern());
return new NumericTickFormatter._internal(formatter);
}
/// Constructs a new [NumericTickFormatter] that formats using [numberFormat].
factory NumericTickFormatter.fromNumberFormat(NumberFormat numberFormat) {
return new NumericTickFormatter._internal(_getFormatter(numberFormat));
}
/// Constructs a new formatter that uses [NumberFormat.compactCurrency].
factory NumericTickFormatter.compactSimpleCurrency() {
return new NumericTickFormatter._internal(
_getFormatter(new NumberFormat.compactCurrency()));
}
/// Returns a [MeasureFormatter] that calls format on [numberFormat].
static MeasureFormatter _getFormatter(NumberFormat numberFormat) {
return (num value) => numberFormat.format(value);
}
@override
String formatValue(num value) => formatter(value);
@override
bool operator ==(other) =>
other is NumericTickFormatter && formatter == other.formatter;
@override
int get hashCode => formatter.hashCode;
}

View File

@@ -0,0 +1,103 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/chart_context.dart' show ChartContext;
import 'axis.dart' show AxisOrientation;
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import 'scale.dart' show MutableScale;
import 'tick.dart' show Tick;
import 'tick_formatter.dart' show TickFormatter;
/// A strategy for selecting values for axis ticks based on the domain values.
///
/// [D] is the domain type.
abstract class TickProvider<D> {
/// Returns a list of ticks in value order that should be displayed.
///
/// This method should not return null. If no ticks are desired an empty list
/// should be returned.
///
/// [graphicsFactory] The graphics factory used for text measurement.
/// [scale] The scale of the data.
/// [formatter] The formatter to use for generating tick labels.
/// [orientation] Orientation of this axis ticks.
/// [tickDrawStrategy] Draw strategy for ticks.
/// [viewportExtensionEnabled] allow extending the viewport for 'niced' ticks.
/// [tickHint] tick values for provider to calculate a desired tick range.
List<Tick<D>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required covariant MutableScale<D> scale,
@required TickFormatter<D> formatter,
@required Map<D, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<D> tickHint,
});
}
/// A base tick provider.
abstract class BaseTickProvider<D> implements TickProvider<D> {
const BaseTickProvider();
/// Create ticks from [domainValues].
List<Tick<D>> createTicks(
List<D> domainValues, {
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required MutableScale<D> scale,
@required TickFormatter<D> formatter,
@required Map<D, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
num stepSize,
}) {
final ticks = <Tick<D>>[];
final labels =
formatter.format(domainValues, formatterValueCache, stepSize: stepSize);
for (var i = 0; i < domainValues.length; i++) {
final value = domainValues[i];
final tick = new Tick(
value: value,
textElement: graphicsFactory.createTextElement(labels[i]),
locationPx: scale[value]);
ticks.add(tick);
}
// Allow draw strategy to decorate the ticks.
tickDrawStrategy.decorateTicks(ticks);
return ticks;
}
}
/// A hint for the tick provider to determine step size and tick count.
class TickHint<D> {
/// The starting hint tick value.
final D start;
/// The ending hint tick value.
final D end;
/// Number of ticks.
final int tickCount;
TickHint(this.start, this.end, {this.tickCount});
}

View File

@@ -0,0 +1,177 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import '../tick.dart' show Tick;
import '../tick_formatter.dart' show TickFormatter;
import '../tick_provider.dart' show TickProvider, TickHint;
import 'date_time_scale.dart' show DateTimeScale;
import 'day_time_stepper.dart' show DayTimeStepper;
import 'hour_time_stepper.dart' show HourTimeStepper;
import 'minute_time_stepper.dart' show MinuteTimeStepper;
import 'month_time_stepper.dart' show MonthTimeStepper;
import 'time_range_tick_provider.dart' show TimeRangeTickProvider;
import 'time_range_tick_provider_impl.dart' show TimeRangeTickProviderImpl;
import 'year_time_stepper.dart' show YearTimeStepper;
/// Tick provider for date and time.
///
/// When determining the ticks for a given domain, the provider will use choose
/// one of the internal tick providers appropriate to the size of the data's
/// domain range. It does this in an attempt to ensure there are at least 3
/// ticks, before jumping to the next more fine grain provider. The 3 tick
/// minimum is not a hard rule as some of the ticks might be eliminated because
/// of collisions, but the data was within the targeted range.
///
/// Once a tick provider is chosen the selection of ticks is done by the child
/// tick provider.
class AutoAdjustingDateTimeTickProvider implements TickProvider<DateTime> {
/// List of tick providers to be selected from.
final List<TimeRangeTickProvider> _potentialTickProviders;
AutoAdjustingDateTimeTickProvider._internal(
List<TimeRangeTickProvider> tickProviders)
: _potentialTickProviders = tickProviders;
/// Creates a default [AutoAdjustingDateTimeTickProvider] for day and time.
factory AutoAdjustingDateTimeTickProvider.createDefault(
DateTimeFactory dateTimeFactory) {
return new AutoAdjustingDateTimeTickProvider._internal([
createYearTickProvider(dateTimeFactory),
createMonthTickProvider(dateTimeFactory),
createDayTickProvider(dateTimeFactory),
createHourTickProvider(dateTimeFactory),
createMinuteTickProvider(dateTimeFactory)
]);
}
/// Creates a default [AutoAdjustingDateTimeTickProvider] for day only.
factory AutoAdjustingDateTimeTickProvider.createWithoutTime(
DateTimeFactory dateTimeFactory) {
return new AutoAdjustingDateTimeTickProvider._internal([
createYearTickProvider(dateTimeFactory),
createMonthTickProvider(dateTimeFactory),
createDayTickProvider(dateTimeFactory)
]);
}
/// Creates [AutoAdjustingDateTimeTickProvider] with custom tick providers.
///
/// [potentialTickProviders] must have at least one [TimeRangeTickProvider]
/// and this list of tick providers are used in the order they are provided.
factory AutoAdjustingDateTimeTickProvider.createWith(
List<TimeRangeTickProvider> potentialTickProviders) {
if (potentialTickProviders == null || potentialTickProviders.isEmpty) {
throw new ArgumentError('At least one TimeRangeTickProvider is required');
}
return new AutoAdjustingDateTimeTickProvider._internal(
potentialTickProviders);
}
/// Generates a list of ticks for the given data which should not collide
/// unless the range is not large enough.
@override
List<Tick<DateTime>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required DateTimeScale scale,
@required TickFormatter<DateTime> formatter,
@required Map<DateTime, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<DateTime> tickHint,
}) {
List<TimeRangeTickProvider> tickProviders;
/// If tick hint is provided, use the closest tick provider, otherwise
/// look through the tick providers for one that provides sufficient ticks
/// for the viewport.
if (tickHint != null) {
tickProviders = [_getClosestTickProvider(tickHint)];
} else {
tickProviders = _potentialTickProviders;
}
final lastTickProvider = tickProviders.last;
final viewport = scale.viewportDomain;
for (final tickProvider in tickProviders) {
final isLastProvider = (tickProvider == lastTickProvider);
if (isLastProvider ||
tickProvider.providesSufficientTicksForRange(viewport)) {
return tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
orientation: orientation,
);
}
}
return <Tick<DateTime>>[];
}
/// Find the closest tick provider based on the tick hint.
TimeRangeTickProvider _getClosestTickProvider(TickHint<DateTime> tickHint) {
final stepSize = ((tickHint.end.difference(tickHint.start).inMilliseconds) /
(tickHint.tickCount - 1))
.round();
int minDifference;
TimeRangeTickProvider closestTickProvider;
for (final tickProvider in _potentialTickProviders) {
final difference =
(stepSize - tickProvider.getClosestStepSize(stepSize)).abs();
if (minDifference == null || minDifference > difference) {
minDifference = difference;
closestTickProvider = tickProvider;
}
}
return closestTickProvider;
}
static TimeRangeTickProvider createYearTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new YearTimeStepper(dateTimeFactory));
static TimeRangeTickProvider createMonthTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new MonthTimeStepper(dateTimeFactory));
static TimeRangeTickProvider createDayTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new DayTimeStepper(dateTimeFactory));
static TimeRangeTickProvider createHourTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new HourTimeStepper(dateTimeFactory));
static TimeRangeTickProvider createMinuteTickProvider(
DateTimeFactory dateTimeFactory) =>
new TimeRangeTickProviderImpl(new MinuteTimeStepper(dateTimeFactory));
}

View File

@@ -0,0 +1,141 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/date_time_factory.dart';
import 'date_time_extents.dart' show DateTimeExtents;
import 'time_stepper.dart'
show TimeStepper, TimeStepIteratorFactory, TimeStepIterator;
/// A base stepper for operating with DateTimeFactory and time range steps.
abstract class BaseTimeStepper implements TimeStepper {
/// The factory to generate a DateTime object.
///
/// This is needed because Dart's DateTime does not handle time zone.
/// There is a time zone aware library that we could use that implements the
/// DateTime interface.
final DateTimeFactory dateTimeFactory;
_TimeStepIteratorFactoryImpl _stepsIterable;
BaseTimeStepper(this.dateTimeFactory);
/// Get the step time before or on the given [time] from [tickIncrement].
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement);
/// Get the next step time after [time] from [tickIncrement].
DateTime getNextStepTime(DateTime time, int tickIncrement);
@override
int getStepCountBetween(DateTimeExtents timeExtent, int tickIncrement) {
checkTickIncrement(tickIncrement);
final min = timeExtent.start;
final max = timeExtent.end;
var time = getStepTimeAfterInclusive(min, tickIncrement);
var cnt = 0;
while (time.compareTo(max) <= 0) {
cnt++;
time = getNextStepTime(time, tickIncrement);
}
return cnt;
}
@override
TimeStepIteratorFactory getSteps(DateTimeExtents timeExtent) {
// Keep the steps iterable unless time extent changes, so the same iterator
// can be used and reset for different increments.
if (_stepsIterable == null || _stepsIterable.timeExtent != timeExtent) {
_stepsIterable = new _TimeStepIteratorFactoryImpl(timeExtent, this);
}
return _stepsIterable;
}
@override
DateTimeExtents updateBoundingSteps(DateTimeExtents timeExtent) {
final stepBefore = getStepTimeBeforeInclusive(timeExtent.start, 1);
final stepAfter = getStepTimeAfterInclusive(timeExtent.end, 1);
return new DateTimeExtents(start: stepBefore, end: stepAfter);
}
DateTime getStepTimeAfterInclusive(DateTime time, int tickIncrement) {
final boundedStart = getStepTimeBeforeInclusive(time, tickIncrement);
if (boundedStart == time) {
return boundedStart;
}
return getNextStepTime(boundedStart, tickIncrement);
}
}
class _TimeStepIteratorImpl implements TimeStepIterator {
final DateTime extentStartTime;
final DateTime extentEndTime;
final BaseTimeStepper stepper;
DateTime _current;
int _tickIncrement = 1;
_TimeStepIteratorImpl(
this.extentStartTime, this.extentEndTime, this.stepper) {
reset(_tickIncrement);
}
@override
bool moveNext() {
if (_current == null) {
_current =
stepper.getStepTimeAfterInclusive(extentStartTime, _tickIncrement);
} else {
_current = stepper.getNextStepTime(_current, _tickIncrement);
}
return _current.compareTo(extentEndTime) <= 0;
}
@override
DateTime get current => _current;
@override
TimeStepIterator reset(int tickIncrement) {
checkTickIncrement(tickIncrement);
_tickIncrement = tickIncrement;
_current = null;
return this;
}
}
class _TimeStepIteratorFactoryImpl extends TimeStepIteratorFactory {
final DateTimeExtents timeExtent;
final _TimeStepIteratorImpl _timeStepIterator;
_TimeStepIteratorFactoryImpl._internal(
_TimeStepIteratorImpl timeStepIterator, this.timeExtent)
: _timeStepIterator = timeStepIterator;
factory _TimeStepIteratorFactoryImpl(
DateTimeExtents timeExtent, BaseTimeStepper stepper) {
final startTime = timeExtent.start;
final endTime = timeExtent.end;
return new _TimeStepIteratorFactoryImpl._internal(
new _TimeStepIteratorImpl(startTime, endTime, stepper), timeExtent);
}
@override
TimeStepIterator get iterator => _timeStepIterator;
}
void checkTickIncrement(int tickIncrement) {
/// tickIncrement must be greater than 0
assert(tickIncrement > 0);
}

View File

@@ -0,0 +1,41 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../axis.dart' show Axis;
import '../tick_formatter.dart' show TickFormatter;
import '../tick_provider.dart' show TickProvider;
import 'auto_adjusting_date_time_tick_provider.dart'
show AutoAdjustingDateTimeTickProvider;
import 'date_time_extents.dart' show DateTimeExtents;
import 'date_time_scale.dart' show DateTimeScale;
import 'date_time_tick_formatter.dart' show DateTimeTickFormatter;
class DateTimeAxis extends Axis<DateTime> {
DateTimeAxis(DateTimeFactory dateTimeFactory,
{TickProvider tickProvider, TickFormatter tickFormatter})
: super(
tickProvider: tickProvider ??
new AutoAdjustingDateTimeTickProvider.createDefault(
dateTimeFactory),
tickFormatter:
tickFormatter ?? new DateTimeTickFormatter(dateTimeFactory),
scale: new DateTimeScale(dateTimeFactory),
);
void setScaleViewport(DateTimeExtents viewport) {
(mutableScale as DateTimeScale).viewportDomain = viewport;
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../scale.dart' show Extents;
class DateTimeExtents extends Extents<DateTime> {
final DateTime start;
final DateTime end;
DateTimeExtents({@required this.start, @required this.end});
@override
bool operator ==(other) {
return other is DateTimeExtents && start == other.start && end == other.end;
}
@override
int get hashCode => (start.hashCode + (end.hashCode * 37));
}

View File

@@ -0,0 +1,138 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../linear/linear_scale.dart' show LinearScale;
import '../numeric_extents.dart' show NumericExtents;
import '../scale.dart'
show MutableScale, StepSizeConfig, RangeBandConfig, ScaleOutputExtent;
import 'date_time_extents.dart' show DateTimeExtents;
/// [DateTimeScale] is a wrapper for [LinearScale].
/// [DateTime] values are converted to millisecondsSinceEpoch and passed to the
/// [LinearScale].
class DateTimeScale extends MutableScale<DateTime> {
final DateTimeFactory dateTimeFactory;
final LinearScale _linearScale;
DateTimeScale(this.dateTimeFactory) : _linearScale = new LinearScale();
DateTimeScale._copy(DateTimeScale other)
: dateTimeFactory = other.dateTimeFactory,
_linearScale = other._linearScale.copy();
@override
num operator [](DateTime domainValue) =>
_linearScale[domainValue.millisecondsSinceEpoch];
@override
DateTime reverse(double pixelLocation) =>
dateTimeFactory.createDateTimeFromMilliSecondsSinceEpoch(
_linearScale.reverse(pixelLocation).round());
@override
void resetDomain() {
_linearScale.resetDomain();
}
@override
set stepSizeConfig(StepSizeConfig config) {
_linearScale.stepSizeConfig = config;
}
@override
StepSizeConfig get stepSizeConfig => _linearScale.stepSizeConfig;
@override
set rangeBandConfig(RangeBandConfig barGroupWidthConfig) {
_linearScale.rangeBandConfig = barGroupWidthConfig;
}
@override
void setViewportSettings(double viewportScale, double viewportTranslatePx) {
_linearScale.setViewportSettings(viewportScale, viewportTranslatePx);
}
@override
set range(ScaleOutputExtent extent) {
_linearScale.range = extent;
}
@override
void addDomain(DateTime domainValue) {
_linearScale.addDomain(domainValue.millisecondsSinceEpoch);
}
@override
void resetViewportSettings() {
_linearScale.resetViewportSettings();
}
DateTimeExtents get viewportDomain {
final extents = _linearScale.viewportDomain;
return new DateTimeExtents(
start: dateTimeFactory
.createDateTimeFromMilliSecondsSinceEpoch(extents.min.toInt()),
end: dateTimeFactory
.createDateTimeFromMilliSecondsSinceEpoch(extents.max.toInt()));
}
set viewportDomain(DateTimeExtents extents) {
_linearScale.viewportDomain = new NumericExtents(
extents.start.millisecondsSinceEpoch,
extents.end.millisecondsSinceEpoch);
}
@override
DateTimeScale copy() => new DateTimeScale._copy(this);
@override
double get viewportTranslatePx => _linearScale.viewportTranslatePx;
@override
double get viewportScalingFactor => _linearScale.viewportScalingFactor;
@override
bool isRangeValueWithinViewport(double rangeValue) =>
_linearScale.isRangeValueWithinViewport(rangeValue);
@override
int compareDomainValueToViewport(DateTime domainValue) => _linearScale
.compareDomainValueToViewport(domainValue.millisecondsSinceEpoch);
@override
double get rangeBand => _linearScale.rangeBand;
@override
double get stepSize => _linearScale.stepSize;
@override
double get domainStepSize => _linearScale.domainStepSize;
@override
RangeBandConfig get rangeBandConfig => _linearScale.rangeBandConfig;
@override
int get rangeWidth => _linearScale.rangeWidth;
@override
ScaleOutputExtent get range => _linearScale.range;
@override
bool canTranslate(DateTime domainValue) =>
_linearScale.canTranslate(domainValue.millisecondsSinceEpoch);
NumericExtents get dataExtent => _linearScale.dataExtent;
}

View File

@@ -0,0 +1,218 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import '../tick_formatter.dart' show TickFormatter;
import 'hour_tick_formatter.dart' show HourTickFormatter;
import 'time_tick_formatter.dart' show TimeTickFormatter;
import 'time_tick_formatter_impl.dart'
show CalendarField, TimeTickFormatterImpl;
/// A [TickFormatter] that formats date/time values based on minimum difference
/// between subsequent ticks.
///
/// This formatter assumes that the Tick values passed in are sorted in
/// increasing order.
///
/// This class is setup with a list of formatters that format the input ticks at
/// a given time resolution. The time resolution which will accurately display
/// the difference between 2 subsequent ticks is picked. Each time resolution
/// can be setup with a [TimeTickFormatter], which is used to format ticks as
/// regular or transition ticks based on whether the tick has crossed the time
/// boundary defined in the [TimeTickFormatter].
class DateTimeTickFormatter implements TickFormatter<DateTime> {
static const int SECOND = 1000;
static const int MINUTE = 60 * SECOND;
static const int HOUR = 60 * MINUTE;
static const int DAY = 24 * HOUR;
/// Used for the case when there is only one formatter.
static const int ANY = -1;
final Map<int, TimeTickFormatter> _timeFormatters;
/// Creates a [DateTimeTickFormatter] that works well with time tick provider
/// classes.
///
/// The default formatter makes assumptions on border cases that time tick
/// providers will still provide ticks that make sense. Example: Tick provider
/// does not provide ticks with 23 hour intervals. For custom tick providers
/// where these assumptions are not correct, please create a custom
/// [TickFormatter].
factory DateTimeTickFormatter(DateTimeFactory dateTimeFactory,
{Map<int, TimeTickFormatter> overrides}) {
final Map<int, TimeTickFormatter> map = {
MINUTE: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'mm',
transitionFormat: 'h mm',
transitionField: CalendarField.hourOfDay),
HOUR: new HourTickFormatter(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'h',
transitionFormat: 'MMM d ha',
noonFormat: 'ha'),
23 * HOUR: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'd',
transitionFormat: 'MMM d',
transitionField: CalendarField.month),
28 * DAY: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'MMM',
transitionFormat: 'MMM yyyy',
transitionField: CalendarField.year),
364 * DAY: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'yyyy',
transitionFormat: 'yyyy',
transitionField: CalendarField.year),
};
// Allow the user to override some of the defaults.
if (overrides != null) {
map.addAll(overrides);
}
return new DateTimeTickFormatter._internal(map);
}
/// Creates a [DateTimeTickFormatter] without the time component.
factory DateTimeTickFormatter.withoutTime(DateTimeFactory dateTimeFactory) {
return new DateTimeTickFormatter._internal({
23 * HOUR: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'd',
transitionFormat: 'MMM d',
transitionField: CalendarField.month),
28 * DAY: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'MMM',
transitionFormat: 'MMM yyyy',
transitionField: CalendarField.year),
365 * DAY: new TimeTickFormatterImpl(
dateTimeFactory: dateTimeFactory,
simpleFormat: 'yyyy',
transitionFormat: 'yyyy',
transitionField: CalendarField.year),
});
}
/// Creates a [DateTimeTickFormatter] that formats all ticks the same.
///
/// Only use this formatter for data with fixed intervals, otherwise use the
/// default, or build from scratch.
///
/// [formatter] The format for all ticks.
factory DateTimeTickFormatter.uniform(TimeTickFormatter formatter) {
return new DateTimeTickFormatter._internal({ANY: formatter});
}
/// Creates a [DateTimeTickFormatter] that formats ticks with [formatters].
///
/// The formatters are expected to be provided with keys in increasing order.
factory DateTimeTickFormatter.withFormatters(
Map<int, TimeTickFormatter> formatters) {
// Formatters must be non empty.
if (formatters == null || formatters.isEmpty) {
throw new ArgumentError('At least one TimeTickFormatter is required.');
}
return new DateTimeTickFormatter._internal(formatters);
}
DateTimeTickFormatter._internal(this._timeFormatters) {
// If there is only one formatter, just use this one and skip this check.
if (_timeFormatters.length == 1) {
return;
}
_checkPositiveAndSorted(_timeFormatters.keys);
}
@override
List<String> format(List<DateTime> tickValues, Map<DateTime, String> cache,
{@required num stepSize}) {
final tickLabels = <String>[];
if (tickValues.isEmpty) {
return tickLabels;
}
// Find the formatter that is the largest interval that has enough
// resolution to describe the difference between ticks. If no such formatter
// exists pick the highest res one.
var formatter = _timeFormatters[_timeFormatters.keys.first];
var formatterFound = false;
if (_timeFormatters.keys.first == ANY) {
formatterFound = true;
} else {
int minTimeBetweenTicks = stepSize.toInt();
// TODO: Skip the formatter if the formatter's step size is
// smaller than the minimum step size of the data.
var keys = _timeFormatters.keys.iterator;
while (keys.moveNext() && !formatterFound) {
if (keys.current > minTimeBetweenTicks) {
formatterFound = true;
} else {
formatter = _timeFormatters[keys.current];
}
}
}
// Format the ticks.
final tickValuesIt = tickValues.iterator;
var tickValue = (tickValuesIt..moveNext()).current;
var prevTickValue = tickValue;
tickLabels.add(formatter.formatFirstTick(tickValue));
while (tickValuesIt.moveNext()) {
tickValue = tickValuesIt.current;
if (formatter.isTransition(tickValue, prevTickValue)) {
tickLabels.add(formatter.formatTransitionTick(tickValue));
} else {
tickLabels.add(formatter.formatSimpleTick(tickValue));
}
prevTickValue = tickValue;
}
return tickLabels;
}
static void _checkPositiveAndSorted(Iterable<int> values) {
final valuesIterator = values.iterator;
var prev = (valuesIterator..moveNext()).current;
var isSorted = true;
// Only need to check the first value, because the values after are expected
// to be greater.
if (prev <= 0) {
throw new ArgumentError('Formatter keys must be positive');
}
while (valuesIterator.moveNext() && isSorted) {
isSorted = prev < valuesIterator.current;
prev = valuesIterator.current;
}
if (!isSorted) {
throw new ArgumentError(
'Formatters must be sorted with keys in increasing order');
}
}
}

View File

@@ -0,0 +1,81 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart' show BaseTimeStepper;
/// Day stepper.
class DayTimeStepper extends BaseTimeStepper {
// TODO: Remove the 14 day increment if we add week stepper.
static const _defaultIncrements = const [1, 2, 3, 7, 14];
static const _hoursInDay = 24;
final List<int> _allowedTickIncrements;
DayTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory DayTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment option.
assert(allowedTickIncrements.isNotEmpty);
// All increments must be > 0.
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
return new DayTimeStepper._internal(dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => _hoursInDay * 3600 * 1000;
@override
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Get the step time before or on the given [time] from [tickIncrement].
///
/// Increments are based off the beginning of the month.
/// Ex. 5 day increments in a month is 1,6,11,16,21,26,31
/// Ex. Time is Aug 20, increment is 1 day. Returns Aug 20.
/// Ex. Time is Aug 20, increment is 2 days. Returns Aug 19 because 2 day
/// increments in a month is 1,3,5,7,9,11,13,15,17,19,21....
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final dayRemainder = (time.day - 1) % tickIncrement;
// Subtract an extra hour in case stepping through a daylight saving change.
final dayBefore = dayRemainder > 0
? time.subtract(new Duration(hours: (_hoursInDay * dayRemainder) - 1))
: time;
// Explicitly leaving off hours and beyond to truncate to start of day.
final stepBefore = dateTimeFactory.createDateTime(
dayBefore.year, dayBefore.month, dayBefore.day);
return stepBefore;
}
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
// Add an extra hour in case stepping through a daylight saving change.
final stepAfter =
time.add(new Duration(hours: (_hoursInDay * tickIncrement) + 1));
// Explicitly leaving off hours and beyond to truncate to start of day.
return dateTimeFactory.createDateTime(
stepAfter.year, stepAfter.month, stepAfter.day);
}
}

View File

@@ -0,0 +1,45 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:intl/intl.dart' show DateFormat;
import 'package:meta/meta.dart' show required;
import '../../../../common/date_time_factory.dart';
import 'time_tick_formatter_impl.dart'
show CalendarField, TimeTickFormatterImpl;
/// Hour specific tick formatter which will format noon differently.
class HourTickFormatter extends TimeTickFormatterImpl {
DateFormat _noonFormat;
HourTickFormatter(
{@required DateTimeFactory dateTimeFactory,
@required String simpleFormat,
@required String transitionFormat,
@required String noonFormat})
: super(
dateTimeFactory: dateTimeFactory,
simpleFormat: simpleFormat,
transitionFormat: transitionFormat,
transitionField: CalendarField.date) {
_noonFormat = dateTimeFactory.createDateFormat(noonFormat);
}
@override
String formatSimpleTick(DateTime date) {
return (date.hour == 12)
? _noonFormat.format(date)
: super.formatSimpleTick(date);
}
}

View File

@@ -0,0 +1,88 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart' show BaseTimeStepper;
/// Hour stepper.
class HourTimeStepper extends BaseTimeStepper {
static const _defaultIncrements = const [1, 2, 3, 4, 6, 12, 24];
static const _hoursInDay = 24;
static const _millisecondsInHour = 3600 * 1000;
final List<int> _allowedTickIncrements;
HourTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory HourTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment option.
assert(allowedTickIncrements.isNotEmpty);
// All increments must be between 1 and 24 inclusive.
assert(allowedTickIncrements
.any((increment) => increment <= 0 || increment > 24) ==
false);
return new HourTimeStepper._internal(
dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => _millisecondsInHour;
@override
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Get the step time before or on the given [time] from [tickIncrement].
///
/// Guarantee a step at the start of the next day.
/// Ex. Time is Aug 20 10 AM, increment is 1 hour. Returns 10 AM.
/// Ex. Time is Aug 20 6 AM, increment is 4 hours. Returns 4 AM.
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final nextDay = dateTimeFactory
.createDateTime(time.year, time.month, time.day)
.add(new Duration(hours: _hoursInDay + 1));
final nextDayStart = dateTimeFactory.createDateTime(
nextDay.year, nextDay.month, nextDay.day);
final hoursToNextDay =
((nextDayStart.millisecondsSinceEpoch - time.millisecondsSinceEpoch) /
_millisecondsInHour)
.ceil();
final hoursRemainder = hoursToNextDay % tickIncrement;
final rewindHours =
hoursRemainder == 0 ? 0 : tickIncrement - hoursRemainder;
final stepBefore = dateTimeFactory.createDateTime(
time.year, time.month, time.day, time.hour - rewindHours);
return stepBefore;
}
/// Get next step time.
///
/// [time] is expected to be a [DateTime] with the hour at start of the hour.
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
return time.add(new Duration(hours: tickIncrement));
}
}

View File

@@ -0,0 +1,78 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart';
/// Minute stepper where ticks generated aligns with the hour.
class MinuteTimeStepper extends BaseTimeStepper {
static const _defaultIncrements = const [5, 10, 15, 20, 30];
static const _millisecondsInMinute = 60 * 1000;
final List<int> _allowedTickIncrements;
MinuteTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory MinuteTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment
assert(allowedTickIncrements.isNotEmpty);
// Increment must be between 1 and 60 inclusive.
assert(allowedTickIncrements
.any((increment) => increment <= 0 || increment > 60) ==
false);
return new MinuteTimeStepper._internal(
dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => _millisecondsInMinute;
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Picks a tick start time that guarantees the start of the hour is included.
///
/// Ex. Time is 3:46, increments is 5 minutes, step before is 3:45, because
/// we can guarantee a step at 4:00.
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final nextHourStart = time.millisecondsSinceEpoch +
(60 - time.minute) * _millisecondsInMinute;
final minutesToNextHour =
((nextHourStart - time.millisecondsSinceEpoch) / _millisecondsInMinute)
.ceil();
final minRemainder = minutesToNextHour % tickIncrement;
final rewindMinutes = minRemainder == 0 ? 0 : tickIncrement - minRemainder;
final stepBefore = dateTimeFactory.createDateTimeFromMilliSecondsSinceEpoch(
time.millisecondsSinceEpoch - rewindMinutes * _millisecondsInMinute);
return stepBefore;
}
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
return time.add(new Duration(minutes: tickIncrement));
}
}

View File

@@ -0,0 +1,77 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart' show BaseTimeStepper;
/// Month stepper.
class MonthTimeStepper extends BaseTimeStepper {
static const _defaultIncrements = const [1, 2, 3, 4, 6, 12];
final List<int> _allowedTickIncrements;
MonthTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory MonthTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment option.
assert(allowedTickIncrements.isNotEmpty);
// All increments must be > 0.
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
return new MonthTimeStepper._internal(
dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => 30 * 24 * 3600 * 1000;
@override
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Guarantee a step ending in the last month of the year.
///
/// If date is 2017 Oct and increments is 6, the step before is 2017 June.
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final monthRemainder = time.month % tickIncrement;
var newMonth = (time.month - monthRemainder) % DateTime.monthsPerYear;
// Handles the last month of the year (December) edge case.
// Ex. When month is December and increment is 1
if (time.month == DateTime.monthsPerYear && newMonth == 0) {
newMonth = DateTime.monthsPerYear;
}
final newYear =
time.year - (monthRemainder / DateTime.monthsPerYear).floor();
return dateTimeFactory.createDateTime(newYear, newMonth);
}
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
final incrementedMonth = time.month + tickIncrement;
final newMonth = incrementedMonth % DateTime.monthsPerYear;
final newYear =
time.year + (incrementedMonth / DateTime.monthsPerYear).floor();
return dateTimeFactory.createDateTime(newYear, newMonth);
}
}

View File

@@ -0,0 +1,29 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../tick_provider.dart' show BaseTickProvider;
import '../time/date_time_extents.dart' show DateTimeExtents;
/// Provides ticks for a particular time unit.
///
/// Used by [AutoAdjustingDateTimeTickProvider].
abstract class TimeRangeTickProvider extends BaseTickProvider<DateTime> {
/// Returns if this tick provider will produce a sufficient number of ticks
/// for [domainExtents].
bool providesSufficientTicksForRange(DateTimeExtents domainExtents);
/// Find the closet step size, from provided step size, in milliseconds.
int getClosestStepSize(int stepSize);
}

View File

@@ -0,0 +1,129 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/chart_context.dart' show ChartContext;
import '../axis.dart' show AxisOrientation;
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
import '../tick.dart' show Tick;
import '../tick_formatter.dart' show TickFormatter;
import '../tick_provider.dart' show TickHint;
import 'date_time_extents.dart' show DateTimeExtents;
import 'date_time_scale.dart' show DateTimeScale;
import 'time_range_tick_provider.dart' show TimeRangeTickProvider;
import 'time_stepper.dart' show TimeStepper;
// Contains all the common code for the time range tick providers.
class TimeRangeTickProviderImpl extends TimeRangeTickProvider {
final int requiredMinimumTicks;
final TimeStepper timeStepper;
TimeRangeTickProviderImpl(this.timeStepper, {this.requiredMinimumTicks = 3});
@override
bool providesSufficientTicksForRange(DateTimeExtents domainExtents) {
final cnt = timeStepper.getStepCountBetween(domainExtents, 1);
return cnt >= requiredMinimumTicks;
}
/// Find the closet step size, from provided step size, in milliseconds.
@override
int getClosestStepSize(int stepSize) {
return timeStepper.typicalStepSizeMs *
_getClosestIncrementFromStepSize(stepSize);
}
// Find the increment that is closest to the step size.
int _getClosestIncrementFromStepSize(int stepSize) {
int minDifference;
int closestIncrement;
for (int increment in timeStepper.allowedTickIncrements) {
final difference =
(stepSize - (timeStepper.typicalStepSizeMs * increment)).abs();
if (minDifference == null || minDifference > difference) {
minDifference = difference;
closestIncrement = increment;
}
}
return closestIncrement;
}
@override
List<Tick<DateTime>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required DateTimeScale scale,
@required TickFormatter<DateTime> formatter,
@required Map<DateTime, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<DateTime> tickHint,
}) {
List<Tick<DateTime>> currentTicks;
final tickValues = <DateTime>[];
final timeStepIt = timeStepper.getSteps(scale.viewportDomain).iterator;
// Try different tickIncrements and choose the first that has no collisions.
// If none exist use the last one which should have the fewest ticks and
// hope that the renderer will resolve collisions.
//
// If a tick hint was provided, use the tick hint to search for the closest
// increment and use that.
List<int> allowedTickIncrements;
if (tickHint != null) {
final stepSize = tickHint.end.difference(tickHint.start).inMilliseconds;
allowedTickIncrements = [_getClosestIncrementFromStepSize(stepSize)];
} else {
allowedTickIncrements = timeStepper.allowedTickIncrements;
}
for (int i = 0; i < allowedTickIncrements.length; i++) {
// Create tick values with a specified increment.
final tickIncrement = allowedTickIncrements[i];
tickValues.clear();
timeStepIt.reset(tickIncrement);
while (timeStepIt.moveNext()) {
tickValues.add(timeStepIt.current);
}
// Create ticks
currentTicks = createTicks(tickValues,
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
stepSize: timeStepper.typicalStepSizeMs * tickIncrement);
// Request collision check from draw strategy.
final collisionReport =
tickDrawStrategy.collides(currentTicks, orientation);
if (!collisionReport.ticksCollide) {
// Return the first non colliding ticks.
return currentTicks;
}
}
// If all ticks collide, return the last generated ticks.
return currentTicks;
}
}

View File

@@ -0,0 +1,60 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'date_time_extents.dart' show DateTimeExtents;
/// Represents the step/tick information for the given time range.
abstract class TimeStepper {
/// Get new bounding extents to the ticks that would contain the given
/// timeExtents.
DateTimeExtents updateBoundingSteps(DateTimeExtents timeExtents);
/// Returns the number steps/ticks are between the given extents inclusive.
///
/// Does not extend the extents to the bounding ticks.
int getStepCountBetween(DateTimeExtents timeExtents, int tickIncrement);
/// Generates an Iterable for iterating over the time steps bounded by the
/// given timeExtents. The desired tickIncrement can be set on the returned
/// [TimeStepIteratorFactory].
TimeStepIteratorFactory getSteps(DateTimeExtents timeExtents);
/// Returns the typical stepSize for this stepper assuming increment by 1.
int get typicalStepSizeMs;
/// An ordered list of step increments that makes sense given the step.
///
/// Example: hours may increment by 1, 2, 3, 4, 6, 12. It doesn't make sense
/// to increment hours by 7.
List<int> get allowedTickIncrements;
}
/// Iterator with a reset function that can be used multiple times to avoid
/// object instantiation during the Android layout/draw phases.
abstract class TimeStepIterator extends Iterator<DateTime> {
/// Reset the iterator and set the tickIncrement to the specified value.
///
/// This method is provided so that the same iterator instance can be used for
/// different tick increments, avoiding object allocation during Android
/// layout/draw phases.
TimeStepIterator reset(int tickIncrement);
}
/// Factory that creates TimeStepIterator with the set tickIncrement value.
abstract class TimeStepIteratorFactory extends Iterable {
/// Get iterator and optionally set the tickIncrement.
@override
TimeStepIterator get iterator;
}

View File

@@ -0,0 +1,31 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/// Formatter of [DateTime] ticks
abstract class TimeTickFormatter {
/// Format for tick that is the first in a set of ticks.
String formatFirstTick(DateTime date);
/// Format for a 'simple' tick.
///
/// Ex. Not a first tick or transition tick.
String formatSimpleTick(DateTime date);
/// Format for a transitional tick.
String formatTransitionTick(DateTime date);
/// Returns true if tick is a transitional tick.
bool isTransition(DateTime tickValue, DateTime prevTickValue);
}

View File

@@ -0,0 +1,100 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:intl/intl.dart' show DateFormat;
import 'package:meta/meta.dart' show required;
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'time_tick_formatter.dart' show TimeTickFormatter;
/// Formatter that can format simple and transition time ticks differently.
class TimeTickFormatterImpl implements TimeTickFormatter {
DateFormat _simpleFormat;
DateFormat _transitionFormat;
final CalendarField transitionField;
/// Create time tick formatter.
///
/// [dateTimeFactory] factory to use to generate the [DateFormat].
/// [simpleFormat] format to use for most ticks.
/// [transitionFormat] format to use when the time unit transitions.
/// For example showing the month with the date for Jan 1.
/// [transitionField] the calendar field that indicates transition.
TimeTickFormatterImpl(
{@required DateTimeFactory dateTimeFactory,
@required String simpleFormat,
@required String transitionFormat,
this.transitionField}) {
_simpleFormat = dateTimeFactory.createDateFormat(simpleFormat);
_transitionFormat = dateTimeFactory.createDateFormat(transitionFormat);
}
@override
String formatFirstTick(DateTime date) => _transitionFormat.format(date);
@override
String formatSimpleTick(DateTime date) => _simpleFormat.format(date);
@override
String formatTransitionTick(DateTime date) => _transitionFormat.format(date);
@override
bool isTransition(DateTime tickValue, DateTime prevTickValue) {
// Transition is always false if no transition field is specified.
if (transitionField == null) {
return false;
}
final prevTransitionFieldValue =
getCalendarField(prevTickValue, transitionField);
final transitionFieldValue = getCalendarField(tickValue, transitionField);
return prevTransitionFieldValue != transitionFieldValue;
}
/// Gets the calendar field for [dateTime].
int getCalendarField(DateTime dateTime, CalendarField field) {
int value;
switch (field) {
case CalendarField.year:
value = dateTime.year;
break;
case CalendarField.month:
value = dateTime.month;
break;
case CalendarField.date:
value = dateTime.day;
break;
case CalendarField.hourOfDay:
value = dateTime.hour;
break;
case CalendarField.minute:
value = dateTime.minute;
break;
case CalendarField.second:
value = dateTime.second;
break;
}
return value;
}
}
enum CalendarField {
year,
month,
date,
hourOfDay,
minute,
second,
}

View File

@@ -0,0 +1,63 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
import 'base_time_stepper.dart' show BaseTimeStepper;
/// Year stepper.
class YearTimeStepper extends BaseTimeStepper {
static const _defaultIncrements = const [1, 2, 5, 10, 50, 100, 500, 1000];
final List<int> _allowedTickIncrements;
YearTimeStepper._internal(
DateTimeFactory dateTimeFactory, List<int> increments)
: _allowedTickIncrements = increments,
super(dateTimeFactory);
factory YearTimeStepper(DateTimeFactory dateTimeFactory,
{List<int> allowedTickIncrements}) {
// Set the default increments if null.
allowedTickIncrements ??= _defaultIncrements;
// Must have at least one increment option.
assert(allowedTickIncrements.isNotEmpty);
// All increments must be > 0.
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
return new YearTimeStepper._internal(
dateTimeFactory, allowedTickIncrements);
}
@override
int get typicalStepSizeMs => 365 * 24 * 3600 * 1000;
@override
List<int> get allowedTickIncrements => _allowedTickIncrements;
/// Guarantees the increment is a factor of the tick value.
///
/// Example: 2017, tick increment of 10, step before is 2010.
@override
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
final yearRemainder = time.year % tickIncrement;
return dateTimeFactory.createDateTime(time.year - yearRemainder);
}
@override
DateTime getNextStepTime(DateTime time, int tickIncrement) {
return dateTimeFactory.createDateTime(time.year + tickIncrement);
}
}

View File

@@ -0,0 +1,468 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap;
import 'dart:math' show Point;
import 'package:meta/meta.dart' show protected;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../../data/series.dart' show Series;
import '../bar/bar_renderer.dart' show BarRenderer;
import '../common/base_chart.dart' show BaseChart;
import '../common/chart_context.dart' show ChartContext;
import '../common/datum_details.dart' show DatumDetails;
import '../common/processed_series.dart' show MutableSeries;
import '../common/selection_model/selection_model.dart' show SelectionModelType;
import '../common/series_renderer.dart' show SeriesRenderer;
import '../layout/layout_config.dart' show LayoutConfig, MarginSpec;
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
import 'axis/axis.dart'
show
Axis,
AxisOrientation,
OrdinalAxis,
NumericAxis,
domainAxisKey,
measureAxisIdKey,
measureAxisKey;
import 'axis/draw_strategy/gridline_draw_strategy.dart'
show GridlineRendererSpec;
import 'axis/draw_strategy/none_draw_strategy.dart' show NoneDrawStrategy;
import 'axis/draw_strategy/small_tick_draw_strategy.dart'
show SmallTickRendererSpec;
import 'axis/spec/axis_spec.dart' show AxisSpec;
class NumericCartesianChart extends CartesianChart<num> {
NumericCartesianChart(
{bool vertical,
LayoutConfig layoutConfig,
NumericAxis primaryMeasureAxis,
NumericAxis secondaryMeasureAxis,
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
: super(
vertical: vertical,
layoutConfig: layoutConfig,
domainAxis: new NumericAxis(),
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes);
@protected
void initDomainAxis() {
_domainAxis.tickDrawStrategy = new SmallTickRendererSpec<num>()
.createDrawStrategy(context, graphicsFactory);
}
}
class OrdinalCartesianChart extends CartesianChart<String> {
OrdinalCartesianChart(
{bool vertical,
LayoutConfig layoutConfig,
NumericAxis primaryMeasureAxis,
NumericAxis secondaryMeasureAxis,
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
: super(
vertical: vertical,
layoutConfig: layoutConfig,
domainAxis: new OrdinalAxis(),
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes);
@protected
void initDomainAxis() {
_domainAxis
..tickDrawStrategy = new SmallTickRendererSpec<String>()
.createDrawStrategy(context, graphicsFactory);
}
}
abstract class CartesianChart<D> extends BaseChart<D> {
static final _defaultLayoutConfig = new LayoutConfig(
topSpec: new MarginSpec.fromPixel(minPixel: 20),
bottomSpec: new MarginSpec.fromPixel(minPixel: 20),
leftSpec: new MarginSpec.fromPixel(minPixel: 20),
rightSpec: new MarginSpec.fromPixel(minPixel: 20),
);
bool vertical;
/// The current domain axis for this chart.
Axis<D> _domainAxis;
/// Temporarily stores the new domain axis that is passed in the constructor
/// and the new domain axis created when [domainAxisSpec] is set to a new
/// spec.
///
/// This step is necessary because the axis cannot be fully configured until
/// [context] is available. [configurationChanged] is called after [context]
/// is available and [_newDomainAxis] will be set to [_domainAxis] and then
/// reset back to null.
Axis<D> _newDomainAxis;
/// The current domain axis spec that was used to configure [_domainAxis].
///
/// This is kept to check if the axis spec has changed when [domainAxisSpec]
/// is set.
AxisSpec<D> _domainAxisSpec;
/// Temporarily stores the new domain axis spec that is passed in when
/// [domainAxisSpec] is set and is different from [_domainAxisSpec]. This spec
/// is then applied to the new domain axis when [configurationChanged] is
/// called.
AxisSpec<D> _newDomainAxisSpec;
final Axis<num> _primaryMeasureAxis;
final Axis<num> _secondaryMeasureAxis;
final LinkedHashMap<String, NumericAxis> _disjointMeasureAxes;
/// If set to true, the vertical axis will render the opposite of the default
/// direction.
bool flipVerticalAxisOutput = false;
bool _usePrimaryMeasureAxis = false;
bool _useSecondaryMeasureAxis = false;
CartesianChart(
{bool vertical,
LayoutConfig layoutConfig,
Axis<D> domainAxis,
NumericAxis primaryMeasureAxis,
NumericAxis secondaryMeasureAxis,
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
: vertical = vertical ?? true,
// [domainAxis] will be set to the new axis in [configurationChanged].
_newDomainAxis = domainAxis,
_primaryMeasureAxis = primaryMeasureAxis ?? new NumericAxis(),
_secondaryMeasureAxis = secondaryMeasureAxis ?? new NumericAxis(),
_disjointMeasureAxes = disjointMeasureAxes ?? <String, NumericAxis>{},
super(layoutConfig: layoutConfig ?? _defaultLayoutConfig) {
// As a convenience for chart configuration, set the paint order on any axis
// that is missing one.
_primaryMeasureAxis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
_secondaryMeasureAxis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
});
}
void init(ChartContext context, GraphicsFactory graphicsFactory) {
super.init(context, graphicsFactory);
_primaryMeasureAxis.context = context;
_primaryMeasureAxis.tickDrawStrategy = new GridlineRendererSpec<num>()
.createDrawStrategy(context, graphicsFactory);
_secondaryMeasureAxis.context = context;
_secondaryMeasureAxis.tickDrawStrategy = new GridlineRendererSpec<num>()
.createDrawStrategy(context, graphicsFactory);
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis.context = context;
axis.tickDrawStrategy =
new NoneDrawStrategy<num>(context, graphicsFactory);
});
}
Axis get domainAxis => _domainAxis;
/// Allows the chart to configure the domain axis when it is created.
@protected
void initDomainAxis();
/// Create a new domain axis and save the new spec to be applied during
/// [configurationChanged].
set domainAxisSpec(AxisSpec axisSpec) {
if (_domainAxisSpec != axisSpec) {
_newDomainAxis = createDomainAxisFromSpec(axisSpec);
_newDomainAxisSpec = axisSpec;
}
}
/// Creates the domain axis spec from provided axis spec.
@protected
Axis<D> createDomainAxisFromSpec(AxisSpec<D> axisSpec) {
return axisSpec.createAxis();
}
@override
void configurationChanged() {
if (_newDomainAxis != null) {
if (_domainAxis != null) {
removeView(_domainAxis);
}
_domainAxis = _newDomainAxis;
_domainAxis
..context = context
..layoutPaintOrder = LayoutViewPaintOrder.domainAxis;
initDomainAxis();
addView(_domainAxis);
_newDomainAxis = null;
}
if (_newDomainAxisSpec != null) {
_domainAxisSpec = _newDomainAxisSpec;
_newDomainAxisSpec.configure(_domainAxis, context, graphicsFactory);
_newDomainAxisSpec = null;
}
}
/// Gets the measure axis matching the provided id.
///
/// If none is provided, this returns the primary measure axis.
Axis getMeasureAxis({String axisId}) {
Axis axis;
if (axisId == Axis.secondaryMeasureAxisId) {
axis = _secondaryMeasureAxis;
} else if (axisId == Axis.primaryMeasureAxisId) {
axis = _primaryMeasureAxis;
} else if (_disjointMeasureAxes[axisId] != null) {
axis = _disjointMeasureAxes[axisId];
}
// If no valid axisId was provided, fall back to primary axis.
axis ??= _primaryMeasureAxis;
return axis;
}
// TODO: Change measure axis spec to create new measure axis.
/// Sets the primary measure axis for the chart, rendered on the start side of
/// the domain axis.
set primaryMeasureAxisSpec(AxisSpec axisSpec) {
axisSpec.configure(_primaryMeasureAxis, context, graphicsFactory);
}
/// Sets the secondary measure axis for the chart, rendered on the end side of
/// the domain axis.
set secondaryMeasureAxisSpec(AxisSpec axisSpec) {
axisSpec.configure(_secondaryMeasureAxis, context, graphicsFactory);
}
/// Sets a map of disjoint measure axes for the chart.
///
/// Disjoint measure axes can be used to scale a sub-set of series on the
/// chart independently from the primary and secondary axes. The general use
/// case for this type of chart is to show differences in the trends of the
/// data, without comparing their absolute values.
///
/// Disjoint axes will not render any tick or gridline elements. With
/// independent scales, there would be a lot of collision in labels were they
/// to do so.
///
/// If any series is rendered with a disjoint axis, it is highly recommended
/// to render all series with disjoint axes. Otherwise, the chart may be
/// visually misleading.
///
/// A [LinkedHashMap] is used to ensure consistent ordering when painting the
/// axes.
set disjointMeasureAxisSpecs(LinkedHashMap<String, AxisSpec> axisSpecs) {
axisSpecs.forEach((String axisId, AxisSpec axisSpec) {
axisSpec.configure(
_disjointMeasureAxes[axisId], context, graphicsFactory);
});
}
@override
MutableSeries<D> makeSeries(Series<dynamic, D> series) {
MutableSeries<D> s = super.makeSeries(series);
s.measureOffsetFn ??= (_) => 0;
// Setup the Axes
s.setAttr(domainAxisKey, domainAxis);
s.setAttr(measureAxisKey,
getMeasureAxis(axisId: series.getAttribute(measureAxisIdKey)));
return s;
}
@override
SeriesRenderer<D> makeDefaultRenderer() {
return new BarRenderer()..rendererId = SeriesRenderer.defaultRendererId;
}
@override
Map<String, List<MutableSeries<D>>> preprocessSeries(
List<MutableSeries<D>> seriesList) {
var rendererToSeriesList = super.preprocessSeries(seriesList);
// Check if primary or secondary measure axis is being used.
for (final series in seriesList) {
final measureAxisId = series.getAttr(measureAxisIdKey);
_usePrimaryMeasureAxis = _usePrimaryMeasureAxis ||
(measureAxisId == null || measureAxisId == Axis.primaryMeasureAxisId);
_useSecondaryMeasureAxis = _useSecondaryMeasureAxis ||
(measureAxisId == Axis.secondaryMeasureAxisId);
}
// Add or remove the primary axis view.
if (_usePrimaryMeasureAxis) {
addView(_primaryMeasureAxis);
} else {
removeView(_primaryMeasureAxis);
}
// Add or remove the secondary axis view.
if (_useSecondaryMeasureAxis) {
addView(_secondaryMeasureAxis);
} else {
removeView(_secondaryMeasureAxis);
}
// Add all disjoint axis views so that their range will be configured.
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
addView(axis);
});
// Reset stale values from previous draw cycles.
domainAxis.resetDomains();
_primaryMeasureAxis.resetDomains();
_secondaryMeasureAxis.resetDomains();
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis.resetDomains();
});
final reverseAxisDirection = context != null && context.isRtl;
if (vertical) {
domainAxis
..axisOrientation = AxisOrientation.bottom
..reverseOutputRange = reverseAxisDirection;
_primaryMeasureAxis
..axisOrientation = (reverseAxisDirection
? AxisOrientation.right
: AxisOrientation.left)
..reverseOutputRange = flipVerticalAxisOutput;
_secondaryMeasureAxis
..axisOrientation = (reverseAxisDirection
? AxisOrientation.left
: AxisOrientation.right)
..reverseOutputRange = flipVerticalAxisOutput;
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis
..axisOrientation = (reverseAxisDirection
? AxisOrientation.left
: AxisOrientation.right)
..reverseOutputRange = flipVerticalAxisOutput;
});
} else {
domainAxis
..axisOrientation = (reverseAxisDirection
? AxisOrientation.right
: AxisOrientation.left)
..reverseOutputRange = flipVerticalAxisOutput;
_primaryMeasureAxis
..axisOrientation = AxisOrientation.bottom
..reverseOutputRange = reverseAxisDirection;
_secondaryMeasureAxis
..axisOrientation = AxisOrientation.top
..reverseOutputRange = reverseAxisDirection;
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis
..axisOrientation = AxisOrientation.top
..reverseOutputRange = reverseAxisDirection;
});
}
// Have each renderer configure the axes with their domain and measure
// values.
rendererToSeriesList
.forEach((String rendererId, List<MutableSeries<D>> seriesList) {
getSeriesRenderer(rendererId).configureDomainAxes(seriesList);
getSeriesRenderer(rendererId).configureMeasureAxes(seriesList);
});
return rendererToSeriesList;
}
@override
void onSkipLayout() {
// Update ticks only when skipping layout.
domainAxis.updateTicks();
if (_usePrimaryMeasureAxis) {
_primaryMeasureAxis.updateTicks();
}
if (_useSecondaryMeasureAxis) {
_secondaryMeasureAxis.updateTicks();
}
_disjointMeasureAxes.forEach((String axisId, NumericAxis axis) {
axis.updateTicks();
});
super.onSkipLayout();
}
@override
void onPostLayout(Map<String, List<MutableSeries<D>>> rendererToSeriesList) {
fireOnAxisConfigured();
super.onPostLayout(rendererToSeriesList);
}
/// Returns a list of datum details from selection model of [type].
@override
List<DatumDetails<D>> getDatumDetails(SelectionModelType type) {
final entries = <DatumDetails<D>>[];
getSelectionModel(type).selectedDatum.forEach((seriesDatum) {
final series = seriesDatum.series;
final datum = seriesDatum.datum;
final datumIndex = seriesDatum.index;
final domain = series.domainFn(datumIndex);
final measure = series.measureFn(datumIndex);
final rawMeasure = series.rawMeasureFn(datumIndex);
final color = series.colorFn(datumIndex);
final domainPosition = series.getAttr(domainAxisKey).getLocation(domain);
final measurePosition =
series.getAttr(measureAxisKey).getLocation(measure);
final chartPosition = new Point<double>(
vertical ? domainPosition : measurePosition,
vertical ? measurePosition : domainPosition);
entries.add(new DatumDetails(
datum: datum,
domain: domain,
measure: measure,
rawMeasure: rawMeasure,
series: series,
color: color,
chartPosition: chartPosition));
});
return entries;
}
}

View File

@@ -0,0 +1,264 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart';
import '../../common/symbol_renderer.dart' show SymbolRenderer;
import '../../data/series.dart' show AccessorFn;
import '../common/base_chart.dart' show BaseChart;
import '../common/processed_series.dart' show MutableSeries;
import '../common/series_renderer.dart' show BaseSeriesRenderer, SeriesRenderer;
import 'axis/axis.dart' show Axis, domainAxisKey, measureAxisKey;
import 'cartesian_chart.dart' show CartesianChart;
abstract class CartesianRenderer<D> extends SeriesRenderer<D> {
void configureDomainAxes(List<MutableSeries<D>> seriesList);
void configureMeasureAxes(List<MutableSeries<D>> seriesList);
}
abstract class BaseCartesianRenderer<D> extends BaseSeriesRenderer<D>
implements CartesianRenderer<D> {
bool _renderingVertically = true;
BaseCartesianRenderer(
{@required String rendererId,
@required int layoutPaintOrder,
SymbolRenderer symbolRenderer})
: super(
rendererId: rendererId,
layoutPaintOrder: layoutPaintOrder,
symbolRenderer: symbolRenderer);
@override
void onAttach(BaseChart<D> chart) {
super.onAttach(chart);
_renderingVertically = (chart as CartesianChart).vertical;
}
bool get renderingVertically => _renderingVertically;
@override
void configureDomainAxes(List<MutableSeries<D>> seriesList) {
seriesList.forEach((MutableSeries<D> series) {
if (series.data.isEmpty) {
return;
}
final domainAxis = series.getAttr(domainAxisKey);
final domainFn = series.domainFn;
final domainLowerBoundFn = series.domainLowerBoundFn;
final domainUpperBoundFn = series.domainUpperBoundFn;
if (domainAxis == null) {
return;
}
if (renderingVertically) {
for (int i = 0; i < series.data.length; i++) {
domainAxis.addDomainValue(domainFn(i));
if (domainLowerBoundFn != null && domainUpperBoundFn != null) {
final domainLowerBound = domainLowerBoundFn(i);
final domainUpperBound = domainUpperBoundFn(i);
if (domainLowerBound != null && domainUpperBound != null) {
domainAxis.addDomainValue(domainLowerBound);
domainAxis.addDomainValue(domainUpperBound);
}
}
}
} else {
// When rendering horizontally, domains are displayed from top to bottom
// in order to match visual display in legend.
for (int i = series.data.length - 1; i >= 0; i--) {
domainAxis.addDomainValue(domainFn(i));
if (domainLowerBoundFn != null && domainUpperBoundFn != null) {
final domainLowerBound = domainLowerBoundFn(i);
final domainUpperBound = domainUpperBoundFn(i);
if (domainLowerBound != null && domainUpperBound != null) {
domainAxis.addDomainValue(domainLowerBound);
domainAxis.addDomainValue(domainUpperBound);
}
}
}
}
});
}
@override
void configureMeasureAxes(List<MutableSeries<D>> seriesList) {
seriesList.forEach((MutableSeries<D> series) {
if (series.data.isEmpty) {
return;
}
final domainAxis = series.getAttr(domainAxisKey);
final domainFn = series.domainFn;
if (domainAxis == null) {
return;
}
final measureAxis = series.getAttr(measureAxisKey);
if (measureAxis == null) {
return;
}
// Only add the measure values for datum who's domain is within the
// domainAxis viewport.
int startIndex =
findNearestViewportStart(domainAxis, domainFn, series.data);
int endIndex = findNearestViewportEnd(domainAxis, domainFn, series.data);
addMeasureValuesFor(series, measureAxis, startIndex, endIndex);
});
}
void addMeasureValuesFor(
MutableSeries<D> series, Axis measureAxis, int startIndex, int endIndex) {
final measureFn = series.measureFn;
final measureOffsetFn = series.measureOffsetFn;
final measureLowerBoundFn = series.measureLowerBoundFn;
final measureUpperBoundFn = series.measureUpperBoundFn;
for (int i = startIndex; i <= endIndex; i++) {
final measure = measureFn(i);
final measureOffset = measureOffsetFn(i);
if (measure != null && measureOffset != null) {
measureAxis.addDomainValue(measure + measureOffset);
if (measureLowerBoundFn != null && measureUpperBoundFn != null) {
measureAxis.addDomainValue(measureLowerBoundFn(i) + measureOffset);
measureAxis.addDomainValue(measureUpperBoundFn(i) + measureOffset);
}
}
}
}
@visibleForTesting
int findNearestViewportStart(
Axis domainAxis, AccessorFn<D> domainFn, List data) {
if (data.isEmpty) {
return null;
}
// Quick optimization for full viewport (likely).
if (domainAxis.compareDomainValueToViewport(domainFn(0)) == 0) {
return 0;
}
var start = 1; // Index zero was already checked for above.
var end = data.length - 1;
// Binary search for the start of the viewport.
while (end >= start) {
int searchIndex = ((end - start) / 2).floor() + start;
int prevIndex = searchIndex - 1;
var comparisonValue =
domainAxis.compareDomainValueToViewport(domainFn(searchIndex));
var prevComparisonValue =
domainAxis.compareDomainValueToViewport(domainFn(prevIndex));
// Found start?
if (prevComparisonValue == -1 && comparisonValue == 0) {
return searchIndex;
}
// Straddling viewport?
// Return previous index as the nearest start of the viewport.
if (comparisonValue == 1 && prevComparisonValue == -1) {
return (searchIndex - 1);
}
// Before start? Update startIndex
if (comparisonValue == -1) {
start = searchIndex + 1;
} else {
// Middle or after viewport? Update endIndex
end = searchIndex - 1;
}
}
// Binary search would reach this point for the edge cases where the domain
// specified is prior or after the domain viewport.
// If domain is prior to the domain viewport, return the first index as the
// nearest viewport start.
// If domain is after the domain viewport, return the last index as the
// nearest viewport start.
final lastComparison =
domainAxis.compareDomainValueToViewport(domainFn(data.length - 1));
return lastComparison == 1 ? (data.length - 1) : 0;
}
@visibleForTesting
int findNearestViewportEnd(
Axis domainAxis, AccessorFn<D> domainFn, List data) {
if (data.isEmpty) {
return null;
}
var start = 1;
var end = data.length - 1;
// Quick optimization for full viewport (likely).
if (domainAxis.compareDomainValueToViewport(domainFn(end)) == 0) {
return end;
}
end = end - 1; // Last index was already checked for above.
// Binary search for the start of the viewport.
while (end >= start) {
int searchIndex = ((end - start) / 2).floor() + start;
int prevIndex = searchIndex - 1;
int comparisonValue =
domainAxis.compareDomainValueToViewport(domainFn(searchIndex));
int prevComparisonValue =
domainAxis.compareDomainValueToViewport(domainFn(prevIndex));
// Found end?
if (prevComparisonValue == 0 && comparisonValue == 1) {
return prevIndex;
}
// Straddling viewport?
// Return the current index as the start of the viewport.
if (comparisonValue == 1 && prevComparisonValue == -1) {
return searchIndex;
}
// After end? Update endIndex
if (comparisonValue == 1) {
end = searchIndex - 1;
} else {
// Middle or before viewport? Update startIndex
start = searchIndex + 1;
}
}
// Binary search would reach this point for the edge cases where the domain
// specified is prior or after the domain viewport.
// If domain is prior to the domain viewport, return the first index as the
// nearest viewport end.
// If domain is after the domain viewport, return the last index as the
// nearest viewport end.
final lastComparison =
domainAxis.compareDomainValueToViewport(domainFn(data.length - 1));
return lastComparison == 1 ? (data.length - 1) : 0;
}
}

View File

@@ -0,0 +1,712 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle, Point;
import 'package:meta/meta.dart' show protected;
import '../../common/gesture_listener.dart' show GestureListener;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/proxy_gesture_listener.dart' show ProxyGestureListener;
import '../../data/series.dart' show Series;
import '../layout/layout_config.dart' show LayoutConfig;
import '../layout/layout_manager.dart' show LayoutManager;
import '../layout/layout_manager_impl.dart' show LayoutManagerImpl;
import '../layout/layout_view.dart' show LayoutView;
import 'behavior/chart_behavior.dart' show ChartBehavior;
import 'chart_canvas.dart' show ChartCanvas;
import 'chart_context.dart' show ChartContext;
import 'datum_details.dart' show DatumDetails;
import 'processed_series.dart' show MutableSeries;
import 'selection_model/selection_model.dart'
show MutableSelectionModel, SelectionModelType;
import 'series_datum.dart' show SeriesDatum;
import 'series_renderer.dart' show SeriesRenderer, rendererIdKey, rendererKey;
typedef BehaviorCreator = ChartBehavior<D> Function<D>();
abstract class BaseChart<D> {
ChartContext context;
/// Internal use only.
GraphicsFactory graphicsFactory;
LayoutManager _layoutManager;
int _chartWidth;
int _chartHeight;
Duration transition = const Duration(milliseconds: 300);
double animationPercent;
bool _animationsTemporarilyDisabled = false;
/// List of series that were passed into the previous draw call.
///
/// This list will be used when redraw is called, to reset the state of all
/// behaviors to the original list.
List<MutableSeries<D>> _originalSeriesList;
/// List of series that are currently drawn on the chart.
///
/// This list should be used by interactive behaviors between chart draw
/// cycles. It may be filtered or modified by some behaviors during the
/// initial draw cycle (e.g. a [Legend] may hide some series).
List<MutableSeries<D>> _currentSeriesList;
Set<String> _usingRenderers = new Set<String>();
Map<String, List<MutableSeries<D>>> _rendererToSeriesList;
final _seriesRenderers = <String, SeriesRenderer<D>>{};
/// Map of named chart behaviors attached to this chart.
final _behaviorRoleMap = <String, ChartBehavior<D>>{};
final _behaviorStack = <ChartBehavior<D>>[];
final _behaviorTappableMap = <String, ChartBehavior<D>>{};
/// Whether or not the chart will respond to tap events.
///
/// This will generally be true if there is a behavior attached to the chart
/// that does something with tap events, such as "click to select data."
bool get isTappable => _behaviorTappableMap.isNotEmpty;
final _gestureProxy = new ProxyGestureListener();
final _selectionModels = <SelectionModelType, MutableSelectionModel<D>>{};
/// Whether data should be selected by nearest domain distance, or by relative
/// distance.
///
/// This should generally be true for chart types that are intended to be
/// aggregated by domain, and false for charts that plot arbitrary x,y data.
/// Scatter plots, for example, may have many overlapping data with the same
/// domain value.
bool get selectNearestByDomain => true;
final _lifecycleListeners = <LifecycleListener<D>>[];
BaseChart({LayoutConfig layoutConfig}) {
_layoutManager = new LayoutManagerImpl(config: layoutConfig);
}
void init(ChartContext context, GraphicsFactory graphicsFactory) {
this.context = context;
// When graphics factory is updated, update all the views.
if (this.graphicsFactory != graphicsFactory) {
this.graphicsFactory = graphicsFactory;
_layoutManager.applyToViews(
(LayoutView view) => view.graphicsFactory = graphicsFactory);
}
configurationChanged();
}
/// Finish configuring components that require context and graphics factory.
///
/// Some components require context and graphics factory to be set again when
/// configuration has changed but the configuration is set prior to the
/// chart first calling init with the context.
void configurationChanged() {}
int get chartWidth => _chartWidth;
int get chartHeight => _chartHeight;
//
// Gesture proxy methods
//
ProxyGestureListener get gestureProxy => _gestureProxy;
/// Add a [GestureListener] to this chart.
GestureListener addGestureListener(GestureListener listener) {
_gestureProxy.add(listener);
return listener;
}
/// Remove a [GestureListener] from this chart.
void removeGestureListener(GestureListener listener) {
_gestureProxy.remove(listener);
}
LifecycleListener addLifecycleListener(LifecycleListener<D> listener) {
_lifecycleListeners.add(listener);
return listener;
}
bool removeLifecycleListener(LifecycleListener<D> listener) =>
_lifecycleListeners.remove(listener);
/// Returns MutableSelectionModel for the given type. Lazy creates one upon first
/// request.
MutableSelectionModel<D> getSelectionModel(SelectionModelType type) {
return _selectionModels.putIfAbsent(
type, () => new MutableSelectionModel<D>());
}
/// Returns a list of datum details from selection model of [type].
List<DatumDetails<D>> getDatumDetails(SelectionModelType type);
//
// Renderer methods
//
set defaultRenderer(SeriesRenderer<D> renderer) {
renderer.rendererId = SeriesRenderer.defaultRendererId;
addSeriesRenderer(renderer);
}
SeriesRenderer<D> get defaultRenderer =>
getSeriesRenderer(SeriesRenderer.defaultRendererId);
void addSeriesRenderer(SeriesRenderer renderer) {
String rendererId = renderer.rendererId;
SeriesRenderer<D> previousRenderer = _seriesRenderers[rendererId];
if (previousRenderer != null) {
removeView(previousRenderer);
previousRenderer.onDetach(this);
}
addView(renderer);
renderer.onAttach(this);
_seriesRenderers[rendererId] = renderer;
}
SeriesRenderer<D> getSeriesRenderer(String rendererId) {
SeriesRenderer<D> renderer = _seriesRenderers[rendererId];
// Special case, if we are asking for the default and we haven't made it
// yet, then make it now.
if (renderer == null) {
if (rendererId == SeriesRenderer.defaultRendererId) {
renderer = makeDefaultRenderer();
defaultRenderer = renderer;
}
}
// TODO: throw error if couldn't find renderer by id?
return renderer;
}
SeriesRenderer<D> makeDefaultRenderer();
bool pointWithinRenderer(Point<double> chartPosition) {
return _usingRenderers.any((String rendererId) =>
getSeriesRenderer(rendererId)
.componentBounds
.containsPoint(chartPosition));
}
/// Retrieves the datum details that are nearest to the given [drawAreaPoint].
///
/// [drawAreaPoint] represents a point in the chart, such as a point that was
/// clicked/tapped on by a user.
///
/// [selectAcrossAllDrawAreaComponents] specifies whether nearest data
/// selection should be done across the combined draw area of all components
/// with series draw areas, or just the chart's primary draw area bounds.
List<DatumDetails<D>> getNearestDatumDetailPerSeries(
Point<double> drawAreaPoint, bool selectAcrossAllDrawAreaComponents) {
// Optionally grab the combined draw area bounds of all components. If this
// is disabled, then we expect each series renderer to filter out the event
// if [chartPoint] is located outside of its own component bounds.
final boundsOverride =
selectAcrossAllDrawAreaComponents ? drawableLayoutAreaBounds : null;
final details = <DatumDetails<D>>[];
_usingRenderers.forEach((String rendererId) {
details.addAll(getSeriesRenderer(rendererId)
.getNearestDatumDetailPerSeries(
drawAreaPoint, selectNearestByDomain, boundsOverride));
});
details.sort((DatumDetails<D> a, DatumDetails<D> b) {
// Sort so that the nearest one is first.
// Special sort, sort by domain distance first, then by measure distance.
if (selectNearestByDomain) {
int domainDiff = a.domainDistance.compareTo(b.domainDistance);
if (domainDiff == 0) {
return a.measureDistance.compareTo(b.measureDistance);
}
return domainDiff;
} else {
return a.relativeDistance.compareTo(b.relativeDistance);
}
});
return details;
}
/// Retrieves the datum details for the current chart selection.
///
/// [selectionModelType] specifies the type of the selection model to use.
List<DatumDetails<D>> getSelectedDatumDetails(
SelectionModelType selectionModelType) {
final details = <DatumDetails<D>>[];
if (_currentSeriesList == null) {
return details;
}
final selectionModel = getSelectionModel(selectionModelType);
if (selectionModel == null || !selectionModel.hasDatumSelection) {
return details;
}
// Pass each selected datum to the appropriate series renderer to get full
// details appropriate to its series type.
for (SeriesDatum<D> seriesDatum in selectionModel.selectedDatum) {
final rendererId = seriesDatum.series.getAttr(rendererIdKey);
details.add(
getSeriesRenderer(rendererId).getDetailsForSeriesDatum(seriesDatum));
}
return details;
}
//
// Behavior methods
//
/// Helper method to create a behavior with congruent types.
///
/// This invokes the provides helper with type parameters that match this
/// chart.
ChartBehavior<D> createBehavior(BehaviorCreator creator) => creator<D>();
/// Attaches a behavior to the chart.
///
/// Setting a new behavior with the same role as a behavior already attached
/// to the chart will replace the old behavior. The old behavior's removeFrom
/// method will be called before we attach the new behavior.
void addBehavior(ChartBehavior<D> behavior) {
final role = behavior.role;
if (role != null && _behaviorRoleMap[role] != behavior) {
// Remove any old behavior with the same role.
removeBehavior(_behaviorRoleMap[role]);
// Add the new behavior.
_behaviorRoleMap[role] = behavior;
}
// Add the behavior if it wasn't already added.
if (!_behaviorStack.contains(behavior)) {
_behaviorStack.add(behavior);
behavior.attachTo(this);
}
}
/// Removes a behavior from the chart.
///
/// Returns true if a behavior was removed, otherwise returns false.
bool removeBehavior(ChartBehavior<D> behavior) {
if (behavior == null) {
return false;
}
final role = behavior?.role;
if (role != null && _behaviorRoleMap[role] == behavior) {
_behaviorRoleMap.remove(role);
}
// Make sure the removed behavior is no longer registered for tap events.
unregisterTappable(behavior);
final wasAttached = _behaviorStack.remove(behavior);
behavior.removeFrom(this);
return wasAttached;
}
/// Tells the chart that this behavior responds to tap events.
///
/// This should only be called after [behavior] has been attached to the chart
/// via [addBehavior].
void registerTappable(ChartBehavior<D> behavior) {
final role = behavior.role;
if (role != null &&
_behaviorRoleMap[role] == behavior &&
_behaviorTappableMap[role] != behavior) {
_behaviorTappableMap[role] = behavior;
}
}
/// Tells the chart that this behavior no longer responds to tap events.
void unregisterTappable(ChartBehavior<D> behavior) {
final role = behavior?.role;
if (role != null && _behaviorTappableMap[role] == behavior) {
_behaviorTappableMap.remove(role);
}
}
/// Returns a list of behaviors that have been added.
List<ChartBehavior<D>> get behaviors => new List.unmodifiable(_behaviorStack);
//
// Layout methods
//
void measure(int width, int height) {
if (_rendererToSeriesList != null) {
_layoutManager.measure(width, height);
}
}
void layout(int width, int height) {
if (_rendererToSeriesList != null) {
layoutInternal(width, height);
onPostLayout(_rendererToSeriesList);
}
}
void layoutInternal(int width, int height) {
_chartWidth = width;
_chartHeight = height;
_layoutManager.layout(width, height);
}
void addView(LayoutView view) {
if (_layoutManager.isAttached(view) == false) {
view.graphicsFactory = graphicsFactory;
_layoutManager.addView(view);
}
}
void removeView(LayoutView view) {
_layoutManager.removeView(view);
}
/// Returns whether or not [point] is within the draw area bounds.
bool withinDrawArea(Point<num> point) {
return _layoutManager.withinDrawArea(point);
}
/// Returns the bounds of the chart draw area.
Rectangle<int> get drawAreaBounds => _layoutManager.drawAreaBounds;
int get marginBottom => _layoutManager.marginBottom;
int get marginLeft => _layoutManager.marginLeft;
int get marginRight => _layoutManager.marginRight;
int get marginTop => _layoutManager.marginTop;
/// Returns the combined bounds of the chart draw area and all layout
/// components that draw series data.
Rectangle<int> get drawableLayoutAreaBounds =>
_layoutManager.drawableLayoutAreaBounds;
//
// Draw methods
//
void draw(List<Series<dynamic, D>> seriesList) {
// Clear the selection model when [seriesList] changes.
for (final selectionModel in _selectionModels.values) {
selectionModel.clearSelection(notifyListeners: false);
}
var processedSeriesList =
new List<MutableSeries<D>>.from(seriesList.map(makeSeries));
// Allow listeners to manipulate the seriesList.
fireOnDraw(processedSeriesList);
// Set an index on the series list.
// This can be used by listeners of selection to determine the order of
// series, because the selection details are not returned in this order.
int seriesIndex = 0;
processedSeriesList.forEach((series) => series.seriesIndex = seriesIndex++);
// Initially save a reference to processedSeriesList. After drawInternal
// finishes, we expect _currentSeriesList to contain a new, possibly
// modified list.
_currentSeriesList = processedSeriesList;
// Store off processedSeriesList for use later during redraw calls. This
// list will not reflect any modifications that were made to
// _currentSeriesList by behaviors during the draw cycle.
_originalSeriesList = processedSeriesList;
drawInternal(processedSeriesList, skipAnimation: false, skipLayout: false);
}
/// Redraws and re-lays-out the chart using the previously rendered layout
/// dimensions.
void redraw({bool skipAnimation = false, bool skipLayout = false}) {
drawInternal(_originalSeriesList,
skipAnimation: skipAnimation, skipLayout: skipLayout);
// Trigger layout and actually redraw the chart.
if (!skipLayout) {
measure(_chartWidth, _chartHeight);
layout(_chartWidth, _chartHeight);
} else {
onSkipLayout();
}
}
void drawInternal(List<MutableSeries<D>> seriesList,
{bool skipAnimation, bool skipLayout}) {
seriesList = seriesList
.map((MutableSeries<D> series) => new MutableSeries<D>.clone(series))
.toList();
// TODO: Handle exiting renderers.
_animationsTemporarilyDisabled = skipAnimation;
configureSeries(seriesList);
// Allow listeners to manipulate the processed seriesList.
fireOnPreprocess(seriesList);
_rendererToSeriesList = preprocessSeries(seriesList);
// Allow listeners to manipulate the processed seriesList.
fireOnPostprocess(seriesList);
_currentSeriesList = seriesList;
}
List<MutableSeries<D>> get currentSeriesList => _currentSeriesList;
MutableSeries<D> makeSeries(Series<dynamic, D> series) {
final s = new MutableSeries<D>(series);
// Setup the Renderer
final rendererId =
series.getAttribute(rendererIdKey) ?? SeriesRenderer.defaultRendererId;
s.setAttr(rendererIdKey, rendererId);
s.setAttr(rendererKey, getSeriesRenderer(rendererId));
return s;
}
/// Preprocess series to assign missing color functions.
void configureSeries(List<MutableSeries<D>> seriesList) {
Map<String, List<MutableSeries<D>>> rendererToSeriesList = {};
// Build map of rendererIds to SeriesLists. This map can't be re-used later
// in the preprocessSeries call because some behaviors might alter the
// seriesList.
seriesList.forEach((MutableSeries<D> series) {
String rendererId = series.getAttr(rendererIdKey);
rendererToSeriesList.putIfAbsent(rendererId, () => []).add(series);
});
// Have each renderer add missing color functions to their seriesLists.
rendererToSeriesList
.forEach((String rendererId, List<MutableSeries<D>> seriesList) {
getSeriesRenderer(rendererId).configureSeries(seriesList);
});
}
/// Preprocess series to allow stacking and other mutations.
///
/// Build a map of rendererId to series.
Map<String, List<MutableSeries<D>>> preprocessSeries(
List<MutableSeries<D>> seriesList) {
Map<String, List<MutableSeries<D>>> rendererToSeriesList = {};
var unusedRenderers = _usingRenderers;
_usingRenderers = new Set<String>();
// Build map of rendererIds to SeriesLists.
seriesList.forEach((MutableSeries<D> series) {
String rendererId = series.getAttr(rendererIdKey);
rendererToSeriesList.putIfAbsent(rendererId, () => []).add(series);
_usingRenderers.add(rendererId);
unusedRenderers.remove(rendererId);
});
// Allow unused renderers to render out content.
unusedRenderers
.forEach((String rendererId) => rendererToSeriesList[rendererId] = []);
// Have each renderer preprocess their seriesLists.
rendererToSeriesList
.forEach((String rendererId, List<MutableSeries<D>> seriesList) {
getSeriesRenderer(rendererId).preprocessSeries(seriesList);
});
return rendererToSeriesList;
}
void onSkipLayout() {
onPostLayout(_rendererToSeriesList);
}
void onPostLayout(Map<String, List<MutableSeries<D>>> rendererToSeriesList) {
// Update each renderer with
rendererToSeriesList
.forEach((String rendererId, List<MutableSeries<D>> seriesList) {
getSeriesRenderer(rendererId).update(seriesList, animatingThisDraw);
});
// Request animation
if (animatingThisDraw) {
animationPercent = 0.0;
context.requestAnimation(this.transition);
} else {
animationPercent = 1.0;
context.requestPaint();
}
_animationsTemporarilyDisabled = false;
}
void paint(ChartCanvas canvas) {
canvas.drawingView = 'BaseView';
_layoutManager.paintOrderedViews.forEach((LayoutView view) {
canvas.drawingView = view.runtimeType.toString();
view.paint(canvas, animatingThisDraw ? animationPercent : 1.0);
});
canvas.drawingView = 'PostRender';
fireOnPostrender(canvas);
canvas.drawingView = null;
if (animationPercent == 1.0) {
fireOnAnimationComplete();
}
}
bool get animatingThisDraw => (transition != null &&
transition.inMilliseconds > 0 &&
!_animationsTemporarilyDisabled);
@protected
fireOnDraw(List<MutableSeries<D>> seriesList) {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onData != null) {
listener.onData(seriesList);
}
});
}
@protected
fireOnPreprocess(List<MutableSeries<D>> seriesList) {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onPreprocess != null) {
listener.onPreprocess(seriesList);
}
});
}
@protected
fireOnPostprocess(List<MutableSeries<D>> seriesList) {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onPostprocess != null) {
listener.onPostprocess(seriesList);
}
});
}
@protected
fireOnAxisConfigured() {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onAxisConfigured != null) {
listener.onAxisConfigured();
}
});
}
@protected
fireOnPostrender(ChartCanvas canvas) {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onPostrender != null) {
listener.onPostrender(canvas);
}
});
}
@protected
fireOnAnimationComplete() {
_lifecycleListeners.forEach((LifecycleListener<D> listener) {
if (listener.onAnimationComplete != null) {
listener.onAnimationComplete();
}
});
}
/// Called to free up any resources due to chart going away.
destroy() {
// Walk them in add order to support behaviors that remove other behaviors.
for (var i = 0; i < _behaviorStack.length; i++) {
_behaviorStack[i].removeFrom(this);
}
_behaviorStack.clear();
_behaviorRoleMap.clear();
_selectionModels.values.forEach((MutableSelectionModel selectionModel) =>
selectionModel.clearAllListeners());
}
}
class LifecycleListener<D> {
/// Called when new data is drawn to the chart (not a redraw).
///
/// This step is good for processing the data (running averages, percentage of
/// first, etc). It can also be used to add Series of data (trend line) or
/// remove a line as mentioned above, removing Series.
final LifecycleSeriesListCallback onData;
/// Called for every redraw given the original SeriesList resulting from the
/// previous onData.
///
/// This step is good for injecting default attributes on the Series before
/// the renderers process the data (ex: before stacking measures).
final LifecycleSeriesListCallback onPreprocess;
/// Called after the chart and renderers get a chance to process the data but
/// before the axes process them.
///
/// This step is good if you need to alter the Series measure values after the
/// renderers have processed them (ex: after stacking measures).
final LifecycleSeriesListCallback onPostprocess;
/// Called after the Axes have been configured.
/// This step is good if you need to use the axes to get any cartesian
/// location information. At this point Axes should be immutable and stable.
final LifecycleEmptyCallback onAxisConfigured;
/// Called after the chart is done rendering passing along the canvas allowing
/// a behavior or other listener to render on top of the chart.
///
/// This is a convenience callback, however if there is any significant canvas
/// interaction or stacking needs, it is preferred that a AplosView/ChartView
/// is added to the chart instead to fully participate in the view stacking.
final LifecycleCanvasCallback onPostrender;
/// Called after animation hits 100%. This allows a behavior or other listener
/// to chain animations to create a multiple step animation transition.
final LifecycleEmptyCallback onAnimationComplete;
LifecycleListener(
{this.onData,
this.onPreprocess,
this.onPostprocess,
this.onAxisConfigured,
this.onPostrender,
this.onAnimationComplete});
}
typedef LifecycleSeriesListCallback<D>(List<MutableSeries<D>> seriesList);
typedef LifecycleCanvasCallback(ChartCanvas canvas);
typedef LifecycleEmptyCallback();

View File

@@ -0,0 +1,97 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/gesture_listener.dart' show GestureListener;
import '../../base_chart.dart' show BaseChart;
import '../chart_behavior.dart' show ChartBehavior;
import 'a11y_node.dart' show A11yNode;
/// The gesture to use for triggering explore mode.
enum ExploreModeTrigger {
pressHold,
tap,
}
/// Chart behavior for adding A11y information.
abstract class A11yExploreBehavior<D> implements ChartBehavior<D> {
/// The gesture that activates explore mode. Defaults to long press.
///
/// Turning on explore mode asks this [A11yExploreBehavior] to generate nodes within
/// this chart.
final ExploreModeTrigger exploreModeTrigger;
/// Minimum width of the bounding box for the a11y focus.
///
/// Must be 1 or higher because invisible semantic nodes should not be added.
final double minimumWidth;
/// Optionally notify the OS when explore mode is enabled.
final String exploreModeEnabledAnnouncement;
/// Optionally notify the OS when explore mode is disabled.
final String exploreModeDisabledAnnouncement;
BaseChart<D> _chart;
GestureListener _listener;
bool _exploreModeOn = false;
A11yExploreBehavior({
this.exploreModeTrigger = ExploreModeTrigger.pressHold,
double minimumWidth,
this.exploreModeEnabledAnnouncement,
this.exploreModeDisabledAnnouncement,
}) : minimumWidth = minimumWidth ?? 1.0 {
assert(this.minimumWidth >= 1.0);
switch (exploreModeTrigger) {
case ExploreModeTrigger.pressHold:
_listener = new GestureListener(onLongPress: _toggleExploreMode);
break;
case ExploreModeTrigger.tap:
_listener = new GestureListener(onTap: _toggleExploreMode);
break;
}
}
bool _toggleExploreMode(_) {
if (_exploreModeOn) {
_exploreModeOn = false;
// Ask native platform to turn off explore mode.
_chart.context.disableA11yExploreMode(
announcement: exploreModeDisabledAnnouncement);
} else {
_exploreModeOn = true;
// Ask native platform to turn on explore mode.
_chart.context.enableA11yExploreMode(createA11yNodes(),
announcement: exploreModeEnabledAnnouncement);
}
return true;
}
/// Returns a list of A11yNodes for this chart.
List<A11yNode> createA11yNodes();
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addGestureListener(_listener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeGestureListener(_listener);
}
}

View File

@@ -0,0 +1,32 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
typedef void OnFocus();
/// Container for accessibility data.
class A11yNode {
/// The bounding box for this node.
final Rectangle<int> boundingBox;
/// The textual description of this node.
final String label;
/// Callback when the A11yNode is focused by the native platform
OnFocus onFocus;
A11yNode(this.label, this.boundingBox, {this.onFocus});
}

View File

@@ -0,0 +1,195 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:meta/meta.dart' show required;
import '../../../cartesian/axis/axis.dart' show ImmutableAxis, domainAxisKey;
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import '../../series_datum.dart' show SeriesDatum;
import 'a11y_explore_behavior.dart'
show A11yExploreBehavior, ExploreModeTrigger;
import 'a11y_node.dart' show A11yNode, OnFocus;
/// Returns a string for a11y vocalization from a list of series datum.
typedef String VocalizationCallback<D>(List<SeriesDatum<D>> seriesDatums);
/// A simple vocalization that returns the domain value to string.
String domainVocalization<D>(List<SeriesDatum<D>> seriesDatums) {
final datumIndex = seriesDatums.first.index;
final domainFn = seriesDatums.first.series.domainFn;
final domain = domainFn(datumIndex);
return domain.toString();
}
/// Behavior that generates semantic nodes for each domain.
class DomainA11yExploreBehavior<D> extends A11yExploreBehavior<D> {
final VocalizationCallback _vocalizationCallback;
LifecycleListener<D> _lifecycleListener;
CartesianChart<D> _chart;
List<MutableSeries<D>> _seriesList;
DomainA11yExploreBehavior(
{VocalizationCallback vocalizationCallback,
ExploreModeTrigger exploreModeTrigger,
double minimumWidth,
String exploreModeEnabledAnnouncement,
String exploreModeDisabledAnnouncement})
: _vocalizationCallback = vocalizationCallback ?? domainVocalization,
super(
exploreModeTrigger: exploreModeTrigger,
minimumWidth: minimumWidth,
exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement,
exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement) {
_lifecycleListener =
new LifecycleListener<D>(onPostprocess: _updateSeriesList);
}
@override
List<A11yNode> createA11yNodes() {
final nodes = <_DomainA11yNode>[];
// Update the selection model when the a11y node has focus.
final selectionModel = _chart.getSelectionModel(SelectionModelType.info);
final domainSeriesDatum = <D, List<SeriesDatum<D>>>{};
for (MutableSeries<D> series in _seriesList) {
for (var index = 0; index < series.data.length; index++) {
final datum = series.data[index];
D domain = series.domainFn(index);
domainSeriesDatum[domain] ??= <SeriesDatum<D>>[];
domainSeriesDatum[domain].add(new SeriesDatum<D>(series, datum));
}
}
domainSeriesDatum.forEach((D domain, List<SeriesDatum<D>> seriesDatums) {
final a11yDescription = _vocalizationCallback(seriesDatums);
final firstSeries = seriesDatums.first.series;
final domainAxis = firstSeries.getAttr(domainAxisKey) as ImmutableAxis<D>;
final location = domainAxis.getLocation(domain);
/// If the step size is smaller than the minimum width, use minimum.
final stepSize = (domainAxis.stepSize > minimumWidth)
? domainAxis.stepSize
: minimumWidth;
nodes.add(new _DomainA11yNode(a11yDescription,
location: location,
stepSize: stepSize,
chartDrawBounds: _chart.drawAreaBounds,
isRtl: _chart.context.isRtl,
renderVertically: _chart.vertical,
onFocus: () => selectionModel.updateSelection(seriesDatums, [])));
});
// The screen reader navigates the nodes based on the order it is returned.
// So if the chart is RTL, then the nodes should be ordered with the right
// most domain first.
//
// If the chart has multiple series and one series is missing the domain
// and it was added later, we still want the domains to be in order.
nodes.sort();
return nodes;
}
void _updateSeriesList(List<MutableSeries<D>> seriesList) {
_seriesList = seriesList;
}
@override
void attachTo(BaseChart<D> chart) {
// Domain selection behavior only works for cartesian charts.
assert(chart is CartesianChart);
_chart = chart as CartesianChart;
chart.addLifecycleListener(_lifecycleListener);
super.attachTo(chart);
}
@override
void removeFrom(BaseChart chart) {
chart.removeLifecycleListener(_lifecycleListener);
}
@override
String get role => 'DomainA11yExplore-${exploreModeTrigger}';
}
/// A11yNode with domain specific information.
class _DomainA11yNode extends A11yNode implements Comparable<_DomainA11yNode> {
// Save location, RTL, and is render vertically for sorting
final double location;
final bool isRtl;
final bool renderVertically;
factory _DomainA11yNode(String label,
{@required double location,
@required double stepSize,
@required Rectangle<int> chartDrawBounds,
@required bool isRtl,
@required bool renderVertically,
OnFocus onFocus}) {
Rectangle<int> boundingBox;
if (renderVertically) {
var left = (location - stepSize / 2).round();
var top = chartDrawBounds.top;
var width = stepSize.round();
var height = chartDrawBounds.height;
boundingBox = new Rectangle(left, top, width, height);
} else {
var left = chartDrawBounds.left;
var top = (location - stepSize / 2).round();
var width = chartDrawBounds.width;
var height = stepSize.round();
boundingBox = new Rectangle(left, top, width, height);
}
return new _DomainA11yNode._internal(label, boundingBox,
location: location,
isRtl: isRtl,
renderVertically: renderVertically,
onFocus: onFocus);
}
_DomainA11yNode._internal(String label, Rectangle<int> boundingBox,
{@required this.location,
@required this.isRtl,
@required this.renderVertically,
OnFocus onFocus})
: super(label, boundingBox, onFocus: onFocus);
@override
int compareTo(_DomainA11yNode other) {
// Ordered by smaller location first, unless rendering vertically and RTL,
// then flip to sort by larger location first.
int result = location.compareTo(other.location);
if (renderVertically && isRtl && result != 0) {
result = -result;
}
return result;
}
}

View File

@@ -0,0 +1,235 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../data/series.dart' show AttributeKey;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../behavior/chart_behavior.dart' show ChartBehavior;
import '../../processed_series.dart' show MutableSeries;
const percentInjectedKey =
const AttributeKey<bool>('PercentInjector.percentInjected');
/// Chart behavior that can inject series or domain percentages into each datum.
///
/// [totalType] configures the type of total to be calculated.
///
/// The measure values of each datum will be replaced by the percent of the
/// total measure value that each represents. The "raw" measure accessor
/// function on [MutableSeries] can still be used to get the original values.
///
/// Note that the results for measureLowerBound and measureUpperBound are not
/// currently well defined when converted into percentage values. This behavior
/// will replace them as percents to prevent bad axis results, but no effort is
/// made to bound them to within a "0 to 100%" data range.
///
/// Note that if the chart has a [Legend] that is capable of hiding series data,
/// then this behavior must be added after the [Legend] to ensure that it
/// calculates values after series have been potentially removed from the list.
class PercentInjector<D> implements ChartBehavior<D> {
LifecycleListener<D> _lifecycleListener;
/// The type of data total to be calculated.
final PercentInjectorTotalType totalType;
/// Constructs a [PercentInjector].
///
/// [totalType] configures the type of data total to be calculated.
PercentInjector({this.totalType = PercentInjectorTotalType.domain}) {
// Set up chart draw cycle listeners.
_lifecycleListener =
new LifecycleListener<D>(onPreprocess: _preProcess, onData: _onData);
}
@override
void attachTo(BaseChart<D> chart) {
chart.addLifecycleListener(_lifecycleListener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeLifecycleListener(_lifecycleListener);
}
/// Resets the state of the behavior when new data is drawn on the chart.
void _onData(List<MutableSeries<D>> seriesList) {
// Reset tracking of percentage injection for new data.
seriesList.forEach((MutableSeries series) {
series.setAttr(percentInjectedKey, false);
});
}
/// Injects percent of domain and/or series accessor functions into each
/// series.
///
/// These are injected in the preProcess phase in case other behaviors modify
/// the [seriesList] between chart redraws.
void _preProcess(List<MutableSeries<D>> seriesList) {
var percentInjected = true;
seriesList.forEach((MutableSeries series) {
percentInjected = percentInjected && series.getAttr(percentInjectedKey);
});
if (percentInjected) {
return;
}
switch (totalType) {
case PercentInjectorTotalType.domain:
case PercentInjectorTotalType.domainBySeriesCategory:
final totalsByDomain = <String, num>{};
final useSeriesCategory =
totalType == PercentInjectorTotalType.domainBySeriesCategory;
// Walk the series and compute the domain total. Series total is
// automatically computed by [MutableSeries].
seriesList.forEach((MutableSeries series) {
final seriesCategory = series.seriesCategory;
final rawMeasureFn = series.rawMeasureFn;
final domainFn = series.domainFn;
for (var index = 0; index < series.data.length; index++) {
final domain = domainFn(index);
var measure = rawMeasureFn(index);
measure ??= 0.0;
final key = useSeriesCategory
? '${seriesCategory}__${domain.toString()}'
: '${domain.toString()}';
if (totalsByDomain[key] != null) {
totalsByDomain[key] = totalsByDomain[key] + measure;
} else {
totalsByDomain[key] = measure;
}
}
});
// Add percent of domain and series accessor functions.
seriesList.forEach((MutableSeries series) {
// Replace the default measure accessor with one that computes the
// percentage.
series.measureFn = (int index) {
final measure = series.rawMeasureFn(index);
if (measure == null || measure == 0.0) {
return 0.0;
}
final domain = series.domainFn(index);
final key = useSeriesCategory
? '${series.seriesCategory}__${domain.toString()}'
: '${domain.toString()}';
return measure / totalsByDomain[key];
};
// Replace the default measure lower bound accessor with one that
// computes the percentage.
if (series.measureLowerBoundFn != null) {
series.measureLowerBoundFn = (int index) {
final measureLowerBound = series.rawMeasureLowerBoundFn(index);
if (measureLowerBound == null || measureLowerBound == 0.0) {
return 0.0;
}
final domain = series.domainFn(index);
final key = useSeriesCategory
? '${series.seriesCategory}__${domain.toString()}'
: '${domain.toString()}';
return measureLowerBound / totalsByDomain[key];
};
}
// Replace the default measure upper bound accessor with one that
// computes the percentage.
if (series.measureUpperBoundFn != null) {
series.measureUpperBoundFn = (int index) {
final measureUpperBound = series.rawMeasureUpperBoundFn(index);
if (measureUpperBound == null || measureUpperBound == 0.0) {
return 0.0;
}
final domain = series.domainFn(index);
final key = useSeriesCategory
? '${series.seriesCategory}__${domain.toString()}'
: '${domain.toString()}';
return measureUpperBound / totalsByDomain[key];
};
}
series.setAttr(percentInjectedKey, true);
});
break;
case PercentInjectorTotalType.series:
seriesList.forEach((MutableSeries series) {
// Replace the default measure accessor with one that computes the
// percentage.
series.measureFn = (int index) =>
series.rawMeasureFn(index) / series.seriesMeasureTotal;
// Replace the default measure lower bound accessor with one that
// computes the percentage.
if (series.measureLowerBoundFn != null) {
series.measureLowerBoundFn = (int index) =>
series.rawMeasureLowerBoundFn(index) /
series.seriesMeasureTotal;
}
// Replace the default measure upper bound accessor with one that
// computes the percentage.
if (series.measureUpperBoundFn != null) {
series.measureUpperBoundFn = (int index) =>
series.rawMeasureUpperBoundFn(index) /
series.seriesMeasureTotal;
}
series.setAttr(percentInjectedKey, true);
});
break;
default:
throw new ArgumentError('Unsupported totalType: ${totalType}');
}
}
@override
String get role => 'PercentInjector';
}
/// Describes the type of data total that will be calculated by PercentInjector.
///
/// [domain] calculates the percentage of each datum's measure value out of the
/// total measure values for all data that share the same domain value.
///
/// [domainBySeriesCategory] calculates the percentage of each datum's measure
/// value out of the total measure values for all data that share the same
/// domain value and seriesCategory value. This should be enabled if the data
/// will be rendered by a series renderer that groups data by both domain and
/// series category, such as the "grouped stacked" mode of [BarRenderer].
///
/// [series] calculates the percentage of each datum's measure value out of the
/// total measure values for all data in that datum's series.
enum PercentInjectorTotalType { domain, domainBySeriesCategory, series }

View File

@@ -0,0 +1,66 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../base_chart.dart';
/// Interface for adding behavior to a chart.
///
/// For example pan and zoom are implemented via behavior strategies.
abstract class ChartBehavior<D> {
String get role;
/// Injects the behavior into a chart.
void attachTo(BaseChart<D> chart);
/// Removes the behavior from a chart.
void removeFrom(BaseChart<D> chart);
}
/// Position of a component within the chart layout.
///
/// Outside positions are [top], [bottom], [start], and [end].
///
/// [top] component positioned at the top, with the chart positioned below the
/// component and height reduced by the height of the component.
/// [bottom] component positioned below the chart, and the chart's height is
/// reduced by the height of the component.
/// [start] component is positioned at the left of the chart (or the right if
/// RTL), the chart's width is reduced by the width of the component.
/// [end] component is positioned at the right of the chart (or the left if
/// RTL), the chart's width is reduced by the width of the component.
/// [inside] component is layered on top of the chart.
enum BehaviorPosition {
top,
bottom,
start,
end,
inside,
}
/// Justification for components positioned outside [BehaviorPosition].
enum OutsideJustification {
startDrawArea,
start,
middleDrawArea,
middle,
endDrawArea,
end,
}
/// Justification for components positioned [BehaviorPosition.inside].
enum InsideJustification {
topStart,
topEnd,
}

View File

@@ -0,0 +1,836 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:meta/meta.dart';
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../../common/text_element.dart'
show MaxWidthStrategy, TextDirection, TextElement;
import '../../../../common/text_style.dart' show TextStyle;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
ViewMeasuredSizes;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../behavior/chart_behavior.dart'
show BehaviorPosition, ChartBehavior, OutsideJustification;
import '../../chart_canvas.dart' show ChartCanvas;
/// Chart behavior that adds title text to a chart. An optional second line of
/// text may be rendered as a sub-title.
///
/// Titles will by default be rendered as the outermost component in the chart
/// margin.
class ChartTitle<D> implements ChartBehavior<D> {
static const _defaultBehaviorPosition = BehaviorPosition.top;
static const _defaultMaxWidthStrategy = MaxWidthStrategy.ellipsize;
static const _defaultTitleDirection = ChartTitleDirection.auto;
static const _defaultTitleOutsideJustification = OutsideJustification.middle;
static final _defaultTitleStyle =
new TextStyleSpec(fontSize: 18, color: StyleFactory.style.tickColor);
static final _defaultSubTitleStyle =
new TextStyleSpec(fontSize: 14, color: StyleFactory.style.tickColor);
static const _defaultInnerPadding = 10;
static const _defaultTitlePadding = 18;
static const _defaultOuterPadding = 10;
/// Stores all of the configured properties of the behavior.
_ChartTitleConfig _config;
BaseChart<D> _chart;
_ChartTitleLayoutView _view;
LifecycleListener<D> _lifecycleListener;
/// Constructs a [ChartTitle].
///
/// [title] contains the text for the chart title.
ChartTitle(String title,
{BehaviorPosition behaviorPosition,
int innerPadding,
int layoutMinSize,
int layoutPreferredSize,
int outerPadding,
MaxWidthStrategy maxWidthStrategy,
ChartTitleDirection titleDirection,
OutsideJustification titleOutsideJustification,
int titlePadding,
TextStyleSpec titleStyleSpec,
String subTitle,
TextStyleSpec subTitleStyleSpec}) {
_config = new _ChartTitleConfig()
..behaviorPosition = behaviorPosition ?? _defaultBehaviorPosition
..innerPadding = innerPadding ?? _defaultInnerPadding
..layoutMinSize = layoutMinSize
..layoutPreferredSize = layoutPreferredSize
..outerPadding = outerPadding ?? _defaultOuterPadding
..maxWidthStrategy = maxWidthStrategy ?? _defaultMaxWidthStrategy
..title = title
..titleDirection = titleDirection ?? _defaultTitleDirection
..titleOutsideJustification =
titleOutsideJustification ?? _defaultTitleOutsideJustification
..titlePadding = titlePadding ?? _defaultTitlePadding
..titleStyleSpec = titleStyleSpec ?? _defaultTitleStyle
..subTitle = subTitle
..subTitleStyleSpec = subTitleStyleSpec ?? _defaultSubTitleStyle;
_lifecycleListener =
new LifecycleListener<D>(onAxisConfigured: _updateViewData);
}
/// Layout position for the title.
BehaviorPosition get behaviorPosition => _config.behaviorPosition;
set behaviorPosition(BehaviorPosition behaviorPosition) {
_config.behaviorPosition = behaviorPosition;
}
/// Minimum size of the legend component. Optional.
///
/// If the legend is positioned in the top or bottom margin, then this
/// configures the legend's height. If positioned in the start or end
/// position, this configures the legend's width.
int get layoutMinSize => _config.layoutMinSize;
set layoutMinSize(int layoutMinSize) {
_config.layoutMinSize = layoutMinSize;
}
/// Preferred size of the legend component. Defaults to 0.
///
/// If the legend is positioned in the top or bottom margin, then this
/// configures the legend's height. If positioned in the start or end
/// position, this configures the legend's width.
int get layoutPreferredSize => _config.layoutPreferredSize;
set layoutPreferredSize(int layoutPreferredSize) {
_config.layoutPreferredSize = layoutPreferredSize;
}
/// Strategy for handling title text that is too large to fit. Defaults to
/// truncating the text with ellipses.
MaxWidthStrategy get maxWidthStrategy => _config.maxWidthStrategy;
set maxWidthStrategy(MaxWidthStrategy maxWidthStrategy) {
_config.maxWidthStrategy = maxWidthStrategy;
}
/// Primary text for the title.
String get title => _config.title;
set title(String title) {
_config.title = title;
}
/// Direction of the chart title text.
///
/// This defaults to horizontal for a title in the top or bottom
/// [behaviorPosition], or vertical for start or end [behaviorPosition].
ChartTitleDirection get titleDirection => _config.titleDirection;
set titleDirection(ChartTitleDirection titleDirection) {
_config.titleDirection = titleDirection;
}
/// Justification of the title text if it is positioned outside of the draw
/// area.
OutsideJustification get titleOutsideJustification =>
_config.titleOutsideJustification;
set titleOutsideJustification(
OutsideJustification titleOutsideJustification) {
_config.titleOutsideJustification = titleOutsideJustification;
}
/// Space between the title and sub-title text, if defined.
///
/// This padding is not used if no sub-title is provided.
int get titlePadding => _config.titlePadding;
set titlePadding(int titlePadding) {
_config.titlePadding = titlePadding;
}
/// Style of the [title] text.
TextStyleSpec get titleStyleSpec => _config.titleStyleSpec;
set titleStyleSpec(TextStyleSpec titleStyleSpec) {
_config.titleStyleSpec = titleStyleSpec;
}
/// Secondary text for the sub-title.
///
/// [subTitle] is rendered on a second line below the [title], and may be
/// styled differently.
String get subTitle => _config.subTitle;
set subTitle(String subTitle) {
_config.subTitle = subTitle;
}
/// Style of the [subTitle] text.
TextStyleSpec get subTitleStyleSpec => _config.subTitleStyleSpec;
set subTitleStyleSpec(TextStyleSpec subTitleStyleSpec) {
_config.subTitleStyleSpec = subTitleStyleSpec;
}
/// Space between the "inside" of the chart, and the title behavior itself.
///
/// This padding is applied to all the edge of the title that is in the
/// direction of the draw area. For a top positioned title, this is applied
/// to the bottom edge. [outerPadding] is applied to the top, left, and right
/// edges.
///
/// If a sub-title is defined, this is the space between the sub-title text
/// and the inside of the chart. Otherwise, it is the space between the title
/// text and the inside of chart.
int get innerPadding => _config.innerPadding;
set innerPadding(int innerPadding) {
_config.innerPadding = innerPadding;
}
/// Space between the "outside" of the chart, and the title behavior itself.
///
/// This padding is applied to all 3 edges of the title that are not in the
/// direction of the draw area. For a top positioned title, this is applied
/// to the top, left, and right edges. [innerPadding] is applied to the
/// bottom edge.
int get outerPadding => _config.outerPadding;
set outerPadding(int outerPadding) {
_config.outerPadding = outerPadding;
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
_view = new _ChartTitleLayoutView<D>(
layoutPaintOrder: LayoutViewPaintOrder.chartTitle,
config: _config,
chart: _chart);
chart.addView(_view);
chart.addLifecycleListener(_lifecycleListener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeView(_view);
chart.removeLifecycleListener(_lifecycleListener);
_chart = null;
}
void _updateViewData() {
_view.config = _config;
}
@override
String get role => 'ChartTitle-${_config?.behaviorPosition}';
bool get isRtl => _chart.context.isRtl;
}
/// Layout view component for [ChartTitle].
class _ChartTitleLayoutView<D> extends LayoutView {
LayoutViewConfig _layoutConfig;
LayoutViewConfig get layoutConfig => _layoutConfig;
/// Stores all of the configured properties of the behavior.
_ChartTitleConfig _config;
BaseChart<D> chart;
bool get isRtl => chart?.context?.isRtl ?? false;
Rectangle<int> _componentBounds;
Rectangle<int> _drawAreaBounds;
GraphicsFactory _graphicsFactory;
/// Cached layout element for the title text.
///
/// This is used to prevent expensive Flutter painter layout calls on every
/// animation frame during the paint cycle. It should never be cached during
/// layout measurement.
TextElement _titleTextElement;
/// Cached layout element for the sub-title text.
///
/// This is used to prevent expensive Flutter painter layout calls on every
/// animation frame during the paint cycle. It should never be cached during
/// layout measurement.
TextElement _subTitleTextElement;
_ChartTitleLayoutView(
{@required int layoutPaintOrder,
@required _ChartTitleConfig config,
@required this.chart})
: this._config = config {
// Set inside body to resolve [_layoutPosition].
_layoutConfig = new LayoutViewConfig(
paintOrder: layoutPaintOrder,
position: _layoutPosition,
positionOrder: LayoutViewPositionOrder.chartTitle);
}
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
/// Sets the configuration for the title behavior.
set config(_ChartTitleConfig config) {
_config = config;
layoutConfig.position = _layoutPosition;
}
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
int minWidth;
int minHeight;
int preferredWidth = 0;
int preferredHeight = 0;
// Always assume that we need outer padding and title padding, but only add
// in the sub-title padding if we have one. Title is required, but sub-title
// is optional.
final totalPadding = _config.outerPadding +
_config.innerPadding +
(_config.subTitle != null ? _config.titlePadding : 0.0);
// Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
// The [GraphicsFactory] is needed so it can't be created earlier.
final textStyle = _getTextStyle(graphicsFactory, _config.titleStyleSpec);
final textElement = graphicsFactory.createTextElement(_config.title)
..maxWidthStrategy = _config.maxWidthStrategy
..textStyle = textStyle;
final subTitleTextStyle =
_getTextStyle(graphicsFactory, _config.subTitleStyleSpec);
final subTitleTextElement =
graphicsFactory.createTextElement(_config.subTitle)
..maxWidthStrategy = _config.maxWidthStrategy
..textStyle = subTitleTextStyle;
final resolvedTitleDirection = _resolvedTitleDirection;
switch (_config.behaviorPosition) {
case BehaviorPosition.bottom:
case BehaviorPosition.top:
final textHeight =
(resolvedTitleDirection == ChartTitleDirection.vertical
? textElement.measurement.horizontalSliceWidth
: textElement.measurement.verticalSliceWidth)
.round();
final subTitleTextHeight = _config.subTitle != null
? (resolvedTitleDirection == ChartTitleDirection.vertical
? subTitleTextElement.measurement.horizontalSliceWidth
: subTitleTextElement.measurement.verticalSliceWidth)
.round()
: 0;
final measuredHeight =
(textHeight + subTitleTextHeight + totalPadding).round();
minHeight = _config.layoutMinSize != null
? min(_config.layoutMinSize, measuredHeight)
: measuredHeight;
preferredWidth = maxWidth;
preferredHeight = _config.layoutPreferredSize != null
? min(_config.layoutPreferredSize, maxHeight)
: measuredHeight;
break;
case BehaviorPosition.end:
case BehaviorPosition.start:
final textWidth =
(resolvedTitleDirection == ChartTitleDirection.vertical
? textElement.measurement.verticalSliceWidth
: textElement.measurement.horizontalSliceWidth)
.round();
final subTitleTextWidth = _config.subTitle != null
? (resolvedTitleDirection == ChartTitleDirection.vertical
? subTitleTextElement.measurement.verticalSliceWidth
: subTitleTextElement.measurement.horizontalSliceWidth)
.round()
: 0;
final measuredWidth =
(textWidth + subTitleTextWidth + totalPadding).round();
minWidth = _config.layoutMinSize != null
? min(_config.layoutMinSize, measuredWidth)
: measuredWidth;
preferredWidth = _config.layoutPreferredSize != null
? min(_config.layoutPreferredSize, maxWidth)
: measuredWidth;
preferredHeight = maxHeight;
break;
case BehaviorPosition.inside:
preferredWidth = _drawAreaBounds != null
? min(_drawAreaBounds.width, maxWidth)
: maxWidth;
preferredHeight = _drawAreaBounds != null
? min(_drawAreaBounds.height, maxHeight)
: maxHeight;
break;
}
// Reset the cached text elements used during the paint step.
_resetTextElementCache();
return new ViewMeasuredSizes(
minWidth: minWidth,
minHeight: minHeight,
preferredWidth: preferredWidth,
preferredHeight: preferredHeight);
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
this._componentBounds = componentBounds;
this._drawAreaBounds = drawAreaBounds;
// Reset the cached text elements used during the paint step.
_resetTextElementCache();
}
@override
void paint(ChartCanvas canvas, double animationPercent) {
final resolvedTitleDirection = _resolvedTitleDirection;
var titleHeight = 0.0;
var subTitleHeight = 0.0;
// First, measure the height of the title and sub-title.
if (_config.title != null) {
// Chart titles do not animate. As an optimization for Flutter, cache the
// [TextElement] to avoid an expensive painter layout operation on
// subsequent animation frames.
if (_titleTextElement == null) {
// Create [TextStyle] from [TextStyleSpec] to be used by all the
// elements. The [GraphicsFactory] is needed so it can't be created
// earlier.
final textStyle =
_getTextStyle(graphicsFactory, _config.titleStyleSpec);
_titleTextElement = graphicsFactory.createTextElement(_config.title)
..maxWidthStrategy = _config.maxWidthStrategy
..textStyle = textStyle;
_titleTextElement.maxWidth =
resolvedTitleDirection == ChartTitleDirection.horizontal
? _componentBounds.width
: _componentBounds.height;
}
// Get the height of the title so that we can off-set both text elements.
titleHeight = _titleTextElement.measurement.verticalSliceWidth;
}
if (_config.subTitle != null) {
// Chart titles do not animate. As an optimization for Flutter, cache the
// [TextElement] to avoid an expensive painter layout operation on
// subsequent animation frames.
if (_subTitleTextElement == null) {
// Create [TextStyle] from [TextStyleSpec] to be used by all the
// elements. The [GraphicsFactory] is needed so it can't be created
// earlier.
final textStyle =
_getTextStyle(graphicsFactory, _config.subTitleStyleSpec);
_subTitleTextElement =
graphicsFactory.createTextElement(_config.subTitle)
..maxWidthStrategy = _config.maxWidthStrategy
..textStyle = textStyle;
_subTitleTextElement.maxWidth =
resolvedTitleDirection == ChartTitleDirection.horizontal
? _componentBounds.width
: _componentBounds.height;
}
// Get the height of the sub-title so that we can off-set both text
// elements.
subTitleHeight = _subTitleTextElement.measurement.verticalSliceWidth;
}
// Draw a title if the text is not empty.
if (_config.title != null) {
final labelPoint = _getLabelPosition(
true,
_componentBounds,
resolvedTitleDirection,
_titleTextElement,
titleHeight,
subTitleHeight);
if (labelPoint != null) {
final rotation = resolvedTitleDirection == ChartTitleDirection.vertical
? -pi / 2
: 0.0;
canvas.drawText(_titleTextElement, labelPoint.x, labelPoint.y,
rotation: rotation);
}
}
// Draw a sub-title if the text is not empty.
if (_config.subTitle != null) {
final labelPoint = _getLabelPosition(
false,
_componentBounds,
resolvedTitleDirection,
_subTitleTextElement,
titleHeight,
subTitleHeight);
if (labelPoint != null) {
final rotation = resolvedTitleDirection == ChartTitleDirection.vertical
? -pi / 2
: 0.0;
canvas.drawText(_subTitleTextElement, labelPoint.x, labelPoint.y,
rotation: rotation);
}
}
}
/// Resets the cached text elements used during the paint step.
void _resetTextElementCache() {
_titleTextElement = null;
_subTitleTextElement = null;
}
/// Get the direction of the title, resolving "auto" position into the
/// appropriate direction for the position of the behavior.
ChartTitleDirection get _resolvedTitleDirection {
var resolvedTitleDirection = _config.titleDirection;
if (resolvedTitleDirection == ChartTitleDirection.auto) {
switch (_config.behaviorPosition) {
case BehaviorPosition.bottom:
case BehaviorPosition.inside:
case BehaviorPosition.top:
resolvedTitleDirection = ChartTitleDirection.horizontal;
break;
case BehaviorPosition.end:
case BehaviorPosition.start:
resolvedTitleDirection = ChartTitleDirection.vertical;
break;
}
}
return resolvedTitleDirection;
}
/// Get layout position from chart title position.
LayoutPosition get _layoutPosition {
LayoutPosition position;
switch (_config.behaviorPosition) {
case BehaviorPosition.bottom:
position = LayoutPosition.Bottom;
break;
case BehaviorPosition.end:
position = isRtl ? LayoutPosition.Left : LayoutPosition.Right;
break;
case BehaviorPosition.inside:
position = LayoutPosition.DrawArea;
break;
case BehaviorPosition.start:
position = isRtl ? LayoutPosition.Right : LayoutPosition.Left;
break;
case BehaviorPosition.top:
position = LayoutPosition.Top;
break;
}
// If we have a "full" [OutsideJustification], convert the layout position
// to the "full" form.
if (_config.titleOutsideJustification == OutsideJustification.start ||
_config.titleOutsideJustification == OutsideJustification.middle ||
_config.titleOutsideJustification == OutsideJustification.end) {
switch (position) {
case LayoutPosition.Bottom:
position = LayoutPosition.FullBottom;
break;
case LayoutPosition.Left:
position = LayoutPosition.FullLeft;
break;
case LayoutPosition.Top:
position = LayoutPosition.FullTop;
break;
case LayoutPosition.Right:
position = LayoutPosition.FullRight;
break;
// Ignore other positions, like DrawArea.
default:
break;
}
}
return position;
}
/// Gets the resolved location for a label element.
Point<int> _getLabelPosition(
bool isPrimaryTitle,
Rectangle<num> bounds,
ChartTitleDirection titleDirection,
TextElement textElement,
double titleHeight,
double subTitleHeight) {
switch (_config.behaviorPosition) {
case BehaviorPosition.bottom:
case BehaviorPosition.top:
return _getHorizontalLabelPosition(isPrimaryTitle, bounds,
titleDirection, textElement, titleHeight, subTitleHeight);
break;
case BehaviorPosition.start:
case BehaviorPosition.end:
return _getVerticalLabelPosition(isPrimaryTitle, bounds, titleDirection,
textElement, titleHeight, subTitleHeight);
break;
case BehaviorPosition.inside:
break;
}
return null;
}
/// Gets the resolved location for a title in the top or bottom margin.
Point<int> _getHorizontalLabelPosition(
bool isPrimaryTitle,
Rectangle<num> bounds,
ChartTitleDirection titleDirection,
TextElement textElement,
double titleHeight,
double subTitleHeight) {
int labelX = 0;
int labelY = 0;
switch (_config.titleOutsideJustification) {
case OutsideJustification.middle:
case OutsideJustification.middleDrawArea:
final textWidth =
(isRtl ? 1 : -1) * textElement.measurement.horizontalSliceWidth / 2;
labelX = (bounds.left + bounds.width / 2 + textWidth).round();
textElement.textDirection =
isRtl ? TextDirection.rtl : TextDirection.ltr;
break;
case OutsideJustification.end:
case OutsideJustification.endDrawArea:
case OutsideJustification.start:
case OutsideJustification.startDrawArea:
final alignLeft = isRtl
? (_config.titleOutsideJustification == OutsideJustification.end ||
_config.titleOutsideJustification ==
OutsideJustification.endDrawArea)
: (_config.titleOutsideJustification ==
OutsideJustification.start ||
_config.titleOutsideJustification ==
OutsideJustification.startDrawArea);
// Don't apply outer padding if we are aligned to the draw area.
final padding = (_config.titleOutsideJustification ==
OutsideJustification.endDrawArea ||
_config.titleOutsideJustification ==
OutsideJustification.startDrawArea)
? 0.0
: _config.outerPadding;
if (alignLeft) {
labelX = (bounds.left + padding).round();
textElement.textDirection = TextDirection.ltr;
} else {
labelX = (bounds.right - padding).round();
textElement.textDirection = TextDirection.rtl;
}
break;
}
// labelY is always relative to the component bounds.
if (_config.behaviorPosition == BehaviorPosition.bottom) {
final padding = _config.innerPadding +
(isPrimaryTitle ? 0 : _config.titlePadding + titleHeight);
labelY = (bounds.top + padding).round();
} else {
var padding = 0.0 + _config.innerPadding;
if (isPrimaryTitle) {
padding +=
((subTitleHeight > 0 ? _config.titlePadding + subTitleHeight : 0) +
titleHeight);
} else {
padding += subTitleHeight;
}
labelY = (bounds.bottom - padding).round();
}
return new Point<int>(labelX, labelY);
}
/// Gets the resolved location for a title in the left or right margin.
Point<int> _getVerticalLabelPosition(
bool isPrimaryTitle,
Rectangle<num> bounds,
ChartTitleDirection titleDirection,
TextElement textElement,
double titleHeight,
double subTitleHeight) {
int labelX = 0;
int labelY = 0;
switch (_config.titleOutsideJustification) {
case OutsideJustification.middle:
case OutsideJustification.middleDrawArea:
final textWidth =
(isRtl ? -1 : 1) * textElement.measurement.horizontalSliceWidth / 2;
labelY = (bounds.top + bounds.height / 2 + textWidth).round();
textElement.textDirection =
isRtl ? TextDirection.rtl : TextDirection.ltr;
break;
case OutsideJustification.end:
case OutsideJustification.endDrawArea:
case OutsideJustification.start:
case OutsideJustification.startDrawArea:
final alignLeft = isRtl
? (_config.titleOutsideJustification == OutsideJustification.end ||
_config.titleOutsideJustification ==
OutsideJustification.endDrawArea)
: (_config.titleOutsideJustification ==
OutsideJustification.start ||
_config.titleOutsideJustification ==
OutsideJustification.startDrawArea);
// Don't apply outer padding if we are aligned to the draw area.
final padding = (_config.titleOutsideJustification ==
OutsideJustification.endDrawArea ||
_config.titleOutsideJustification ==
OutsideJustification.startDrawArea)
? 0.0
: _config.outerPadding;
if (alignLeft) {
labelY = (bounds.bottom - padding).round();
textElement.textDirection = TextDirection.ltr;
} else {
labelY = (bounds.top + padding).round();
textElement.textDirection = TextDirection.rtl;
}
break;
}
// labelX is always relative to the component bounds.
if (_layoutPosition == LayoutPosition.Right ||
_layoutPosition == LayoutPosition.FullRight) {
final padding = _config.outerPadding +
(isPrimaryTitle ? 0 : _config.titlePadding + titleHeight);
labelX = (bounds.left + padding).round();
} else {
final padding = _config.outerPadding +
titleHeight +
(isPrimaryTitle
? (subTitleHeight > 0 ? _config.titlePadding + subTitleHeight : 0)
: 0.0);
labelX = (bounds.right - padding).round();
}
return new Point<int>(labelX, labelY);
}
// Helper function that converts [TextStyleSpec] to [TextStyle].
TextStyle _getTextStyle(
GraphicsFactory graphicsFactory, TextStyleSpec labelSpec) {
return graphicsFactory.createTextPaint()
..color = labelSpec?.color ?? StyleFactory.style.tickColor
..fontFamily = labelSpec?.fontFamily
..fontSize = labelSpec?.fontSize ?? 18;
}
@override
Rectangle<int> get componentBounds => this._drawAreaBounds;
@override
bool get isSeriesRenderer => false;
}
/// Configuration object for [ChartTitle].
class _ChartTitleConfig {
BehaviorPosition behaviorPosition;
int layoutMinSize;
int layoutPreferredSize;
MaxWidthStrategy maxWidthStrategy;
String title;
ChartTitleDirection titleDirection;
OutsideJustification titleOutsideJustification;
TextStyleSpec titleStyleSpec;
String subTitle;
TextStyleSpec subTitleStyleSpec;
int innerPadding;
int titlePadding;
int outerPadding;
}
/// Direction of the title text on the chart.
enum ChartTitleDirection {
/// Automatically assign a direction based on the [RangeAnnotationAxisType].
///
/// [horizontal] for measure axes, or [vertical] for domain axes.
auto,
/// Text flows parallel to the x axis.
horizontal,
/// Text flows parallel to the y axis.
vertical,
}

View File

@@ -0,0 +1,83 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../base_chart.dart' show BaseChart, LifecycleListener;
import '../processed_series.dart' show MutableSeries;
import '../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import 'chart_behavior.dart' show ChartBehavior;
/// Chart behavior that monitors the specified [SelectionModel] and darkens the
/// color for selected data.
///
/// This is typically used for bars and pies to highlight segments.
///
/// It is used in combination with SelectNearest to update the selection model
/// and expand selection out to the domain value.
class DomainHighlighter<D> implements ChartBehavior<D> {
final SelectionModelType selectionModelType;
BaseChart<D> _chart;
LifecycleListener<D> _lifecycleListener;
DomainHighlighter([this.selectionModelType = SelectionModelType.info]) {
_lifecycleListener =
new LifecycleListener<D>(onPostprocess: _updateColorFunctions);
}
void _selectionChanged(SelectionModel selectionModel) {
_chart.redraw(skipLayout: true, skipAnimation: true);
}
void _updateColorFunctions(List<MutableSeries<D>> seriesList) {
SelectionModel selectionModel =
_chart.getSelectionModel(selectionModelType);
seriesList.forEach((MutableSeries<D> series) {
final origColorFn = series.colorFn;
if (origColorFn != null) {
series.colorFn = (int index) {
final origColor = origColorFn(index);
if (selectionModel.isDatumSelected(series, index)) {
return origColor.darker;
} else {
return origColor;
}
};
}
});
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addLifecycleListener(_lifecycleListener);
chart
.getSelectionModel(selectionModelType)
.addSelectionChangedListener(_selectionChanged);
}
@override
void removeFrom(BaseChart chart) {
chart
.getSelectionModel(selectionModelType)
.removeSelectionChangedListener(_selectionChanged);
chart.removeLifecycleListener(_lifecycleListener);
}
@override
String get role => 'domainHighlight-${selectionModelType.toString()}';
}

View File

@@ -0,0 +1,74 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../base_chart.dart' show BaseChart, LifecycleListener;
import '../processed_series.dart' show MutableSeries;
import '../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import '../series_datum.dart' show SeriesDatumConfig;
import 'chart_behavior.dart' show ChartBehavior;
/// Behavior that sets initial selection.
class InitialSelection<D> implements ChartBehavior<D> {
final SelectionModelType selectionModelType;
/// List of series id of initially selected series.
final List<String> selectedSeriesConfig;
/// List of [SeriesDatumConfig] that represents the initially selected datums.
final List<SeriesDatumConfig> selectedDataConfig;
BaseChart<D> _chart;
LifecycleListener<D> _lifecycleListener;
bool _firstDraw = true;
// TODO : When the series changes, if the user does not also
// change the index the wrong item could be highlighted.
InitialSelection(
{this.selectionModelType = SelectionModelType.info,
this.selectedDataConfig,
this.selectedSeriesConfig}) {
_lifecycleListener = new LifecycleListener<D>(onData: _setInitialSelection);
}
void _setInitialSelection(List<MutableSeries<D>> seriesList) {
if (!_firstDraw) {
return;
}
_firstDraw = false;
final immutableModel = new SelectionModel<D>.fromConfig(
selectedDataConfig, selectedSeriesConfig, seriesList);
_chart.getSelectionModel(selectionModelType).updateSelection(
immutableModel.selectedDatum, immutableModel.selectedSeries,
notifyListeners: false);
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addLifecycleListener(_lifecycleListener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeLifecycleListener(_lifecycleListener);
_chart = null;
}
@override
String get role => 'InitialSelection-${selectionModelType.toString()}}';
}

View File

@@ -0,0 +1,104 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import 'legend.dart';
import 'legend_entry_generator.dart';
import 'per_datum_legend_entry_generator.dart';
/// Datum legend behavior for charts.
///
/// By default this behavior creates one legend entry per datum in the first
/// series rendered on the chart.
///
/// TODO: Allows for hovering over a datum in legend to highlight
/// corresponding datum in draw area.
///
/// TODO: Implement tap to hide individual data in the series.
class DatumLegend<D> extends Legend<D> {
/// Whether or not the series legend should show measures on datum selection.
bool _showMeasures;
DatumLegend({
SelectionModelType selectionModelType,
LegendEntryGenerator<D> legendEntryGenerator,
MeasureFormatter measureFormatter,
MeasureFormatter secondaryMeasureFormatter,
bool showMeasures,
LegendDefaultMeasure legendDefaultMeasure,
TextStyleSpec entryTextStyle,
}) : super(
selectionModelType: selectionModelType ?? SelectionModelType.info,
legendEntryGenerator:
legendEntryGenerator ?? new PerDatumLegendEntryGenerator(),
entryTextStyle: entryTextStyle) {
// Call the setters that include the setting for default.
this.showMeasures = showMeasures;
this.legendDefaultMeasure = legendDefaultMeasure;
this.measureFormatter = measureFormatter;
this.secondaryMeasureFormatter = secondaryMeasureFormatter;
}
/// Whether or not the legend should show measures.
///
/// By default this is false, measures are not shown. When set to true, the
/// default behavior is to show measure only if there is selected data.
/// Please set [legendDefaultMeasure] to something other than none to enable
/// showing measures when there is no selection.
///
/// If [showMeasure] is set to null, it is changed to the default of false.
bool get showMeasures => _showMeasures;
set showMeasures(bool showMeasures) {
_showMeasures = showMeasures ?? false;
}
/// Option to show measures when selection is null.
///
/// By default this is set to none, so no measures are shown when there is
/// no selection.
///
/// If [legendDefaultMeasure] is set to null, it is changed to the default of
/// none.
LegendDefaultMeasure get legendDefaultMeasure =>
legendEntryGenerator.legendDefaultMeasure;
set legendDefaultMeasure(LegendDefaultMeasure legendDefaultMeasure) {
legendEntryGenerator.legendDefaultMeasure =
legendDefaultMeasure ?? LegendDefaultMeasure.none;
}
/// Formatter for measure values.
///
/// This is optional. The default formatter formats measure values with
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
/// returned.
set measureFormatter(MeasureFormatter formatter) {
legendEntryGenerator.measureFormatter =
formatter ?? defaultLegendMeasureFormatter;
}
/// Formatter for measure values of series that uses the secondary axis.
///
/// This is optional. The default formatter formats measure values with
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
/// returned.
set secondaryMeasureFormatter(MeasureFormatter formatter) {
legendEntryGenerator.secondaryMeasureFormatter =
formatter ?? defaultLegendMeasureFormatter;
}
}

View File

@@ -0,0 +1,433 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:meta/meta.dart' show protected;
import 'package:intl/intl.dart';
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPositionOrder,
LayoutViewPaintOrder,
ViewMeasuredSizes;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../chart_canvas.dart' show ChartCanvas;
import '../../chart_context.dart' show ChartContext;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import '../chart_behavior.dart'
show
BehaviorPosition,
ChartBehavior,
InsideJustification,
OutsideJustification;
import 'legend_entry.dart';
import 'legend_entry_generator.dart';
/// Legend behavior for charts.
///
/// Since legends are desired to be customizable, building and displaying the
/// visual content of legends is done on the native platforms. This allows users
/// to specify customized content for legends using the native platform (ex. for
/// Flutter, using widgets).
abstract class Legend<D> implements ChartBehavior<D>, LayoutView {
final SelectionModelType selectionModelType;
final legendState = new LegendState<D>();
final LegendEntryGenerator<D> legendEntryGenerator;
String _title;
BaseChart _chart;
LifecycleListener<D> _lifecycleListener;
Rectangle<int> _componentBounds;
Rectangle<int> _drawAreaBounds;
GraphicsFactory _graphicsFactory;
BehaviorPosition _behaviorPosition = BehaviorPosition.end;
OutsideJustification _outsideJustification =
OutsideJustification.startDrawArea;
InsideJustification _insideJustification = InsideJustification.topStart;
LegendCellPadding _cellPadding;
LegendCellPadding _legendPadding;
TextStyleSpec _titleTextStyle;
LegendTapHandling _legendTapHandling = LegendTapHandling.hide;
List<MutableSeries<D>> _currentSeriesList;
/// Save this in order to check if series list have changed and regenerate
/// the legend entries.
List<MutableSeries<D>> _postProcessSeriesList;
static final _decimalPattern = new NumberFormat.decimalPattern();
/// Default measure formatter for legends.
@protected
String defaultLegendMeasureFormatter(num value) {
return (value == null) ? '' : _decimalPattern.format(value);
}
Legend({this.selectionModelType, this.legendEntryGenerator, entryTextStyle}) {
_lifecycleListener = new LifecycleListener(
onPostprocess: _postProcess, onPreprocess: _preProcess, onData: onData);
legendEntryGenerator.entryTextStyle = entryTextStyle;
}
String get title => _title;
/// Sets title text to display before legend entries.
set title(String title) {
_title = title;
}
BehaviorPosition get behaviorPosition => _behaviorPosition;
set behaviorPosition(BehaviorPosition behaviorPosition) {
_behaviorPosition = behaviorPosition;
}
OutsideJustification get outsideJustification => _outsideJustification;
set outsideJustification(OutsideJustification outsideJustification) {
_outsideJustification = outsideJustification;
}
InsideJustification get insideJustification => _insideJustification;
set insideJustification(InsideJustification insideJustification) {
_insideJustification = insideJustification;
}
LegendCellPadding get cellPadding => _cellPadding;
set cellPadding(LegendCellPadding cellPadding) {
_cellPadding = cellPadding;
}
LegendCellPadding get legendPadding => _legendPadding;
set legendPadding(LegendCellPadding legendPadding) {
_legendPadding = legendPadding;
}
LegendTapHandling get legendTapHandling => _legendTapHandling;
/// Text style of the legend entry text.
TextStyleSpec get entryTextStyle => legendEntryGenerator.entryTextStyle;
set entryTextStyle(TextStyleSpec entryTextStyle) {
legendEntryGenerator.entryTextStyle = entryTextStyle;
}
/// Text style of the legend title text.
TextStyleSpec get titleTextStyle => _titleTextStyle;
set titleTextStyle(TextStyleSpec titleTextStyle) {
_titleTextStyle = titleTextStyle;
}
/// Configures the behavior of the legend when the user taps/clicks on an
/// entry. Defaults to no behavior.
///
/// Tapping on a legend entry will update the data visible on the chart. For
/// example, when [LegendTapHandling.hide] is configured, the series or datum
/// associated with that entry will be removed from the chart. Tapping on that
/// entry a second time will make the data visible again.
set legendTapHandling(LegendTapHandling legendTapHandling) {
_legendTapHandling = legendTapHandling;
}
/// Resets any hidden series data when new data is drawn on the chart.
@protected
void onData(List<MutableSeries<D>> seriesList) {}
/// Store off a copy of the series list for use when we render the legend.
void _preProcess(List<MutableSeries<D>> seriesList) {
_currentSeriesList = new List.from(seriesList);
preProcessSeriesList(seriesList);
}
/// Overridable method that may be used by concrete [Legend] instances to
/// manipulate the series list.
@protected
void preProcessSeriesList(List<MutableSeries<D>> seriesList) {}
/// Build LegendEntries from list of series.
void _postProcess(List<MutableSeries<D>> seriesList) {
// Get the selection model directly from chart on post process.
//
// This is because if initial selection is set as a behavior, it will be
// handled during onData. onData is prior to this behavior's postProcess
// call, so the selection will have changed prior to the entries being
// generated.
final selectionModel = chart.getSelectionModel(selectionModelType);
// Update entries if the selection model is different because post
// process is called on each draw cycle, so this is called on each animation
// frame and we don't want to update and request the native platform to
// rebuild if nothing has changed.
//
// Also update legend entries if the series list has changed.
if (legendState._selectionModel != selectionModel ||
_postProcessSeriesList != seriesList) {
legendState._legendEntries =
legendEntryGenerator.getLegendEntries(_currentSeriesList);
legendState._selectionModel = selectionModel;
_postProcessSeriesList = seriesList;
_updateLegendEntries();
}
}
// need to handle when series data changes, selection should be reset
/// Update the legend state with [selectionModel] and request legend update.
void _selectionChanged(SelectionModel selectionModel) {
legendState._selectionModel = selectionModel;
_updateLegendEntries();
}
ChartContext get chartContext => _chart.context;
/// Internally update legend entries, before calling [updateLegend] that
/// notifies the native platform.
void _updateLegendEntries() {
legendEntryGenerator.updateLegendEntries(legendState._legendEntries,
legendState._selectionModel, chart.currentSeriesList);
updateLegend();
}
/// Requires override to show in native platform
void updateLegend() {}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addLifecycleListener(_lifecycleListener);
chart
.getSelectionModel(selectionModelType)
.addSelectionChangedListener(_selectionChanged);
chart.addView(this);
}
@override
void removeFrom(BaseChart chart) {
chart
.getSelectionModel(selectionModelType)
.removeSelectionChangedListener(_selectionChanged);
chart.removeLifecycleListener(_lifecycleListener);
chart.removeView(this);
}
@protected
BaseChart get chart => _chart;
@override
String get role => 'legend-${selectionModelType.toString()}';
bool get isRtl => _chart.context.isRtl;
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
@override
LayoutViewConfig get layoutConfig {
return new LayoutViewConfig(
position: _layoutPosition,
positionOrder: LayoutViewPositionOrder.legend,
paintOrder: LayoutViewPaintOrder.legend);
}
/// Get layout position from legend position.
LayoutPosition get _layoutPosition {
LayoutPosition position;
switch (_behaviorPosition) {
case BehaviorPosition.bottom:
position = LayoutPosition.Bottom;
break;
case BehaviorPosition.end:
position = isRtl ? LayoutPosition.Left : LayoutPosition.Right;
break;
case BehaviorPosition.inside:
position = LayoutPosition.DrawArea;
break;
case BehaviorPosition.start:
position = isRtl ? LayoutPosition.Right : LayoutPosition.Left;
position = isRtl ? LayoutPosition.Right : LayoutPosition.Left;
break;
case BehaviorPosition.top:
position = LayoutPosition.Top;
break;
}
return position;
}
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
// Native child classes should override this method to return real
// measurements.
return new ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
_componentBounds = componentBounds;
_drawAreaBounds = drawAreaBounds;
updateLegend();
}
@override
void paint(ChartCanvas canvas, double animationPercent) {}
@override
Rectangle<int> get componentBounds => _componentBounds;
@override
bool get isSeriesRenderer => false;
// Gets the draw area bounds for native legend content to position itself
// accordingly.
Rectangle<int> get drawAreaBounds => _drawAreaBounds;
}
/// Stores legend data used by native legend content builder.
class LegendState<D> {
List<LegendEntry<D>> _legendEntries;
SelectionModel _selectionModel;
List<LegendEntry<D>> get legendEntries => _legendEntries;
SelectionModel get selectionModel => _selectionModel;
}
/// Stores legend cell padding, in percents or pixels.
///
/// If a percent is specified, it takes precedence over a flat pixel value.
class LegendCellPadding {
final double bottomPct;
final double bottomPx;
final double leftPct;
final double leftPx;
final double rightPct;
final double rightPx;
final double topPct;
final double topPx;
/// Creates padding in percents from the left, top, right, and bottom.
const LegendCellPadding.fromLTRBPct(
this.leftPct, this.topPct, this.rightPct, this.bottomPct)
: leftPx = null,
topPx = null,
rightPx = null,
bottomPx = null;
/// Creates padding in pixels from the left, top, right, and bottom.
const LegendCellPadding.fromLTRBPx(
this.leftPx, this.topPx, this.rightPx, this.bottomPx)
: leftPct = null,
topPct = null,
rightPct = null,
bottomPct = null;
/// Creates padding in percents from the top, right, bottom, and left.
const LegendCellPadding.fromTRBLPct(
this.topPct, this.rightPct, this.bottomPct, this.leftPct)
: topPx = null,
rightPx = null,
bottomPx = null,
leftPx = null;
/// Creates padding in pixels from the top, right, bottom, and left.
const LegendCellPadding.fromTRBLPx(
this.topPx, this.rightPx, this.bottomPx, this.leftPx)
: topPct = null,
rightPct = null,
bottomPct = null,
leftPct = null;
/// Creates cell padding where all the offsets are `value` in percent.
///
/// ## Sample code
///
/// Typical eight percent margin on all sides:
///
/// ```dart
/// const LegendCellPadding.allPct(8.0)
/// ```
const LegendCellPadding.allPct(double value)
: leftPct = value,
topPct = value,
rightPct = value,
bottomPct = value,
leftPx = null,
topPx = null,
rightPx = null,
bottomPx = null;
/// Creates cell padding where all the offsets are `value` in pixels.
///
/// ## Sample code
///
/// Typical eight-pixel margin on all sides:
///
/// ```dart
/// const LegendCellPadding.allPx(8.0)
/// ```
const LegendCellPadding.allPx(double value)
: leftPx = value,
topPx = value,
rightPx = value,
bottomPx = value,
leftPct = null,
topPct = null,
rightPct = null,
bottomPct = null;
double bottom(num height) =>
bottomPct != null ? bottomPct * height : bottomPx;
double left(num width) => leftPct != null ? leftPct * width : leftPx;
double right(num width) => rightPct != null ? rightPct * width : rightPx;
double top(num height) => topPct != null ? topPct * height : topPx;
}
/// Options for behavior of tapping/clicking on entries in the legend.
enum LegendTapHandling {
/// No associated behavior.
none,
/// Hide elements on the chart associated with this legend entry.
hide,
}

View File

@@ -0,0 +1,85 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../../common/color.dart';
import '../../../../common/symbol_renderer.dart';
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../processed_series.dart' show ImmutableSeries;
import '../../series_renderer.dart' show rendererKey;
/// Holder for the information used for a legend row.
///
/// [T] the datum class type for the series passed in.
/// [D] the domain class type for the datum.
class LegendEntry<D> {
final String label;
final ImmutableSeries<D> series;
final dynamic datum;
final int datumIndex;
final D domain;
final Color color;
final TextStyleSpec textStyle;
double value;
String formattedValue;
bool isSelected;
/// Zero based index for the row where this legend appears in the legend.
int rowNumber;
/// Zero based index for the column where this legend appears in the legend.
int columnNumber;
/// Total number of rows in the legend.
int rowCount;
/// Total number of columns in the legend.
int columnCount;
/// Indicates whether this is in the first row of a tabular layout.
bool inFirstRow;
/// Indicates whether this is in the first column of a tabular layout.
bool inFirstColumn;
/// Indicates whether this is in the last row of a tabular layout.
bool inLastRow;
/// Indicates whether this is in the last column of a tabular layout.
bool inLastColumn;
// TODO: Forward the default formatters from series and allow for
// native legends to provide separate formatters.
LegendEntry(this.series, this.label,
{this.datum,
this.datumIndex,
this.domain,
this.value,
this.color,
this.textStyle,
this.isSelected = false,
this.rowNumber,
this.columnNumber,
this.rowCount,
this.columnCount,
this.inFirstRow,
this.inFirstColumn,
this.inLastRow,
this.inLastColumn});
/// Get the native symbol renderer stored in the series.
SymbolRenderer get symbolRenderer =>
series.getAttr(rendererKey).symbolRenderer;
}

View File

@@ -0,0 +1,68 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart';
import 'legend_entry.dart';
/// A strategy for generating a list of [LegendEntry] based on the series drawn.
///
/// [D] the domain class type for the datum.
abstract class LegendEntryGenerator<D> {
/// Generates a list of legend entries based on the series drawn on the chart.
///
/// [seriesList] Processed series list.
List<LegendEntry<D>> getLegendEntries(List<MutableSeries<D>> seriesList);
/// Update the list of legend entries based on the selection model.
///
/// [legendEntries] Existing legend entries to update.
/// [selectionModel] Selection model to query selected state.
/// [seriesList] Processed series list.
void updateLegendEntries(List<LegendEntry<D>> legendEntries,
SelectionModel<D> selectionModel, List<MutableSeries<D>> seriesList);
MeasureFormatter get measureFormatter;
set measureFormatter(MeasureFormatter formatter);
MeasureFormatter get secondaryMeasureFormatter;
set secondaryMeasureFormatter(MeasureFormatter formatter);
LegendDefaultMeasure get legendDefaultMeasure;
set legendDefaultMeasure(LegendDefaultMeasure noSelectionMeasure);
TextStyleSpec get entryTextStyle;
set entryTextStyle(TextStyleSpec entryTextStyle);
}
/// Options for calculating what measures are shown when there is no selection.
enum LegendDefaultMeasure {
// No measures are shown where there is no selection.
none,
// Sum of all measure values for the series.
sum,
// Average of all measure values for the series.
average,
// The first measure value of the series.
firstValue,
// The last measure value of the series.
lastValue,
}

View File

@@ -0,0 +1,146 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//import 'dart:collection' show HashSet;
import '../../../cartesian/axis/axis.dart' show Axis, measureAxisIdKey;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../processed_series.dart' show ImmutableSeries, MutableSeries;
import '../../selection_model/selection_model.dart';
import 'legend_entry.dart';
import 'legend_entry_generator.dart';
/// A strategy for generating a list of [LegendEntry] per series data drawn.
///
/// [D] the domain class type for the datum.
class PerDatumLegendEntryGenerator<D> implements LegendEntryGenerator<D> {
TextStyleSpec entryTextStyle;
MeasureFormatter measureFormatter;
MeasureFormatter secondaryMeasureFormatter;
/// Option for showing measures when there is no selection.
LegendDefaultMeasure legendDefaultMeasure;
@override
List<LegendEntry<D>> getLegendEntries(List<MutableSeries<D>> seriesList) {
final legendEntries = <LegendEntry<D>>[];
final series = seriesList[0];
for (var i = 0; i < series.data.length; i++) {
legendEntries.add(new LegendEntry<D>(
series, series.domainFn(i).toString(),
color: series.colorFn(i),
datum: series.data[i],
datumIndex: i,
textStyle: entryTextStyle));
}
// Update with measures only if showing measure on no selection.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
_updateFromSeriesList(legendEntries, seriesList);
}
return legendEntries;
}
@override
void updateLegendEntries(List<LegendEntry<D>> legendEntries,
SelectionModel<D> selectionModel, List<MutableSeries<D>> seriesList) {
if (selectionModel.hasAnySelection) {
_updateFromSelection(legendEntries, selectionModel);
} else {
// Update with measures only if showing measure on no selection.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
_updateFromSeriesList(legendEntries, seriesList);
} else {
_resetLegendEntryMeasures(legendEntries);
}
}
}
/// Update legend entries with measures of the selected datum
void _updateFromSelection(
List<LegendEntry<D>> legendEntries, SelectionModel<D> selectionModel) {
// Given that each legend entry only has one datum associated with it, any
// option for [legendDefaultMeasure] essentially boils down to just showing
// the measure value.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
for (var entry in legendEntries) {
final series = entry.series;
final measure = series.measureFn(entry.datumIndex);
entry.value = measure.toDouble();
entry.formattedValue = _getFormattedMeasureValue(series, measure);
entry.isSelected = selectionModel.selectedSeries
.any((selectedSeries) => series.id == selectedSeries.id);
}
}
}
void _resetLegendEntryMeasures(List<LegendEntry<D>> legendEntries) {
for (LegendEntry<D> entry in legendEntries) {
entry.value = null;
entry.formattedValue = null;
entry.isSelected = false;
}
}
/// Update each legend entry by calculating measure values in [seriesList].
///
/// This method calculates the legend's measure value to show when there is no
/// selection. The type of calculation is based on the [legendDefaultMeasure]
/// value.
void _updateFromSeriesList(
List<LegendEntry<D>> legendEntries, List<MutableSeries<D>> seriesList) {
// Given that each legend entry only has one datum associated with it, any
// option for [legendDefaultMeasure] essentially boils down to just showing
// the measure value.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
for (var entry in legendEntries) {
final series = entry.series;
final measure = series.measureFn(entry.datumIndex);
entry.value = measure.toDouble();
entry.formattedValue = _getFormattedMeasureValue(series, measure);
entry.isSelected = false;
}
}
}
/// Formats the measure value using the appropriate measure formatter
/// function for the series.
String _getFormattedMeasureValue(ImmutableSeries series, num measure) {
return (series.getAttr(measureAxisIdKey) == Axis.secondaryMeasureAxisId)
? secondaryMeasureFormatter(measure)
: measureFormatter(measure);
}
@override
bool operator ==(Object other) {
return other is PerDatumLegendEntryGenerator &&
measureFormatter == other.measureFormatter &&
secondaryMeasureFormatter == other.secondaryMeasureFormatter &&
legendDefaultMeasure == other.legendDefaultMeasure &&
entryTextStyle == other.entryTextStyle;
}
@override
int get hashCode {
int hashcode = measureFormatter?.hashCode ?? 0;
hashcode = (hashcode * 37) + secondaryMeasureFormatter.hashCode;
hashcode = (hashcode * 37) + legendDefaultMeasure.hashCode;
hashcode = (hashcode * 37) + entryTextStyle.hashCode;
return hashcode;
}
}

View File

@@ -0,0 +1,190 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show HashSet;
import '../../../cartesian/axis/axis.dart' show Axis, measureAxisIdKey;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart';
import '../../series_datum.dart' show SeriesDatum;
import 'legend_entry.dart';
import 'legend_entry_generator.dart';
/// A strategy for generating a list of [LegendEntry] per series drawn.
///
/// [D] the domain class type for the datum.
class PerSeriesLegendEntryGenerator<D> implements LegendEntryGenerator<D> {
TextStyleSpec entryTextStyle;
MeasureFormatter measureFormatter;
MeasureFormatter secondaryMeasureFormatter;
/// Option for showing measures when there is no selection.
LegendDefaultMeasure legendDefaultMeasure;
@override
List<LegendEntry<D>> getLegendEntries(List<MutableSeries<D>> seriesList) {
final legendEntries = seriesList
.map((series) => new LegendEntry<D>(series, series.displayName,
color: series.colorFn(0), textStyle: entryTextStyle))
.toList();
// Update with measures only if showing measure on no selection.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
_updateFromSeriesList(legendEntries, seriesList);
}
return legendEntries;
}
@override
void updateLegendEntries(List<LegendEntry<D>> legendEntries,
SelectionModel<D> selectionModel, List<MutableSeries<D>> seriesList) {
if (selectionModel.hasAnySelection) {
_updateFromSelection(legendEntries, selectionModel);
} else {
// Update with measures only if showing measure on no selection.
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
_updateFromSeriesList(legendEntries, seriesList);
} else {
_resetLegendEntryMeasures(legendEntries);
}
}
}
/// Update legend entries with measures of the selected datum
void _updateFromSelection(
List<LegendEntry<D>> legendEntries, SelectionModel<D> selectionModel) {
// Map of series ID to the total selected measure value for that series.
final seriesAndMeasure = <String, num>{};
// Hash set of series ID's that use the secondary measure axis
final secondaryAxisSeriesIDs = new HashSet<String>();
for (SeriesDatum<D> selectedDatum in selectionModel.selectedDatum) {
final series = selectedDatum.series;
final seriesId = series.id;
final measure = series.measureFn(selectedDatum.index) ?? 0;
seriesAndMeasure[seriesId] = seriesAndMeasure.containsKey(seriesId)
? seriesAndMeasure[seriesId] + measure
: measure;
if (series.getAttr(measureAxisIdKey) == Axis.secondaryMeasureAxisId) {
secondaryAxisSeriesIDs.add(seriesId);
}
}
for (var entry in legendEntries) {
final seriesId = entry.series.id;
final measureValue = seriesAndMeasure[seriesId]?.toDouble();
final formattedValue = secondaryAxisSeriesIDs.contains(seriesId)
? secondaryMeasureFormatter(measureValue)
: measureFormatter(measureValue);
entry.value = measureValue;
entry.formattedValue = formattedValue;
entry.isSelected = selectionModel.selectedSeries
.any((selectedSeries) => entry.series.id == selectedSeries.id);
}
}
void _resetLegendEntryMeasures(List<LegendEntry<D>> legendEntries) {
for (LegendEntry<D> entry in legendEntries) {
entry.value = null;
entry.formattedValue = null;
entry.isSelected = false;
}
}
/// Update each legend entry by calculating measure values in [seriesList].
///
/// This method calculates the legend's measure value to show when there is no
/// selection. The type of calculation is based on the [legendDefaultMeasure]
/// value.
void _updateFromSeriesList(
List<LegendEntry<D>> legendEntries, List<MutableSeries<D>> seriesList) {
// Helper function to sum up the measure values
num getMeasureTotal(MutableSeries<D> series) {
var measureTotal = 0.0;
for (var i = 0; i < series.data.length; i++) {
measureTotal += series.measureFn(i);
}
return measureTotal;
}
// Map of series ID to the calculated measure for that series.
final seriesAndMeasure = <String, double>{};
// Map of series ID and the formatted measure for that series.
final seriesAndFormattedMeasure = <String, String>{};
for (MutableSeries<D> series in seriesList) {
final seriesId = series.id;
num calculatedMeasure;
switch (legendDefaultMeasure) {
case LegendDefaultMeasure.sum:
calculatedMeasure = getMeasureTotal(series);
break;
case LegendDefaultMeasure.average:
calculatedMeasure = getMeasureTotal(series) / series.data.length;
break;
case LegendDefaultMeasure.firstValue:
calculatedMeasure = series.measureFn(0);
break;
case LegendDefaultMeasure.lastValue:
calculatedMeasure = series.measureFn(series.data.length - 1);
break;
case LegendDefaultMeasure.none:
// [calculatedMeasure] intentionally left null, since we do not want
// to show any measures.
break;
}
seriesAndMeasure[seriesId] = calculatedMeasure?.toDouble();
seriesAndFormattedMeasure[seriesId] =
(series.getAttr(measureAxisIdKey) == Axis.secondaryMeasureAxisId)
? secondaryMeasureFormatter(calculatedMeasure)
: measureFormatter(calculatedMeasure);
}
for (var entry in legendEntries) {
final seriesId = entry.series.id;
entry.value = seriesAndMeasure[seriesId];
entry.formattedValue = seriesAndFormattedMeasure[seriesId];
entry.isSelected = false;
}
}
@override
bool operator ==(Object other) {
return other is PerSeriesLegendEntryGenerator &&
measureFormatter == other.measureFormatter &&
secondaryMeasureFormatter == other.secondaryMeasureFormatter &&
legendDefaultMeasure == other.legendDefaultMeasure &&
entryTextStyle == other.entryTextStyle;
}
@override
int get hashCode {
int hashcode = measureFormatter?.hashCode ?? 0;
hashcode = (hashcode * 37) + secondaryMeasureFormatter.hashCode;
hashcode = (hashcode * 37) + legendDefaultMeasure.hashCode;
hashcode = (hashcode * 37) + entryTextStyle.hashCode;
return hashcode;
}
}

View File

@@ -0,0 +1,171 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show protected;
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../../datum_details.dart' show MeasureFormatter;
import '../../processed_series.dart' show MutableSeries;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import 'legend.dart';
import 'legend_entry_generator.dart';
import 'per_series_legend_entry_generator.dart';
// TODO: Allows for hovering over a series in legend to highlight
// corresponding series in draw area.
/// Series legend behavior for charts.
///
/// By default this behavior creates a legend entry per series.
class SeriesLegend<D> extends Legend<D> {
/// List of currently hidden series, by ID.
final _hiddenSeriesList = new Set<String>();
/// List of series IDs that should be hidden by default.
List<String> _defaultHiddenSeries;
/// Whether or not the series legend should show measures on datum selection.
bool _showMeasures;
SeriesLegend({
SelectionModelType selectionModelType,
LegendEntryGenerator<D> legendEntryGenerator,
MeasureFormatter measureFormatter,
MeasureFormatter secondaryMeasureFormatter,
bool showMeasures,
LegendDefaultMeasure legendDefaultMeasure,
TextStyleSpec entryTextStyle,
}) : super(
selectionModelType: selectionModelType ?? SelectionModelType.info,
legendEntryGenerator:
legendEntryGenerator ?? new PerSeriesLegendEntryGenerator(),
entryTextStyle: entryTextStyle) {
// Call the setters that include the setting for default.
this.showMeasures = showMeasures;
this.legendDefaultMeasure = legendDefaultMeasure;
this.measureFormatter = measureFormatter;
this.secondaryMeasureFormatter = secondaryMeasureFormatter;
}
/// Sets a list of series IDs that should be hidden by default on first chart
/// draw.
///
/// This will also reset the current list of hidden series, filling it in with
/// the new default list.
set defaultHiddenSeries(List<String> defaultHiddenSeries) {
_defaultHiddenSeries = defaultHiddenSeries;
_hiddenSeriesList.clear();
if (_defaultHiddenSeries != null) {
_defaultHiddenSeries.forEach(hideSeries);
}
}
/// Gets a list of series IDs that should be hidden by default on first chart
/// draw.
List<String> get defaultHiddenSeries => _defaultHiddenSeries;
/// Whether or not the legend should show measures.
///
/// By default this is false, measures are not shown. When set to true, the
/// default behavior is to show measure only if there is selected data.
/// Please set [legendDefaultMeasure] to something other than none to enable
/// showing measures when there is no selection.
///
/// If [showMeasure] is set to null, it is changed to the default of false.
bool get showMeasures => _showMeasures;
set showMeasures(bool showMeasures) {
_showMeasures = showMeasures ?? false;
}
/// Option to show measures when selection is null.
///
/// By default this is set to none, so no measures are shown when there is
/// no selection.
///
/// If [legendDefaultMeasure] is set to null, it is changed to the default of
/// none.
LegendDefaultMeasure get legendDefaultMeasure =>
legendEntryGenerator.legendDefaultMeasure;
set legendDefaultMeasure(LegendDefaultMeasure legendDefaultMeasure) {
legendEntryGenerator.legendDefaultMeasure =
legendDefaultMeasure ?? LegendDefaultMeasure.none;
}
/// Formatter for measure values.
///
/// This is optional. The default formatter formats measure values with
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
/// returned.
set measureFormatter(MeasureFormatter formatter) {
legendEntryGenerator.measureFormatter =
formatter ?? defaultLegendMeasureFormatter;
}
/// Formatter for measure values of series that uses the secondary axis.
///
/// This is optional. The default formatter formats measure values with
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
/// returned.
set secondaryMeasureFormatter(MeasureFormatter formatter) {
legendEntryGenerator.secondaryMeasureFormatter =
formatter ?? defaultLegendMeasureFormatter;
}
/// Remove series IDs from the currently hidden list if those series have been
/// removed from the chart data. The goal is to allow any metric that is
/// removed from a chart, and later re-added to it, to be visible to the user.
@override
void onData(List<MutableSeries<D>> seriesList) {
// If a series was removed from the chart, remove it from our current list
// of hidden series.
final seriesIds = seriesList.map((MutableSeries<D> series) => series.id);
_hiddenSeriesList.removeWhere((String id) => !seriesIds.contains(id));
}
@override
void preProcessSeriesList(List<MutableSeries<D>> seriesList) {
seriesList.removeWhere((MutableSeries<D> series) {
return _hiddenSeriesList.contains(series.id);
});
}
/// Hides the data for a series on the chart by [seriesId].
///
/// The entry in the legend for this series will be grayed out to indicate
/// that it is hidden.
@protected
void hideSeries(String seriesId) {
_hiddenSeriesList.add(seriesId);
}
/// Shows the data for a series on the chart by [seriesId].
///
/// The entry in the legend for this series will be returned to its normal
/// color if it was previously hidden.
@protected
void showSeries(String seriesId) {
_hiddenSeriesList.removeWhere((String id) => id == seriesId);
}
/// Returns whether or not a given series [seriesId] is currently hidden.
bool isSeriesHidden(String seriesId) {
return _hiddenSeriesList.contains(seriesId);
}
}

View File

@@ -0,0 +1,697 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap;
import 'dart:math' show max, min, Point, Rectangle;
import 'package:meta/meta.dart';
import '../../../common/color.dart' show Color;
import '../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../common/style/style_factory.dart' show StyleFactory;
import '../../../common/symbol_renderer.dart'
show CircleSymbolRenderer, SymbolRenderer;
import '../../cartesian/axis/axis.dart'
show ImmutableAxis, domainAxisKey, measureAxisKey;
import '../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
ViewMeasuredSizes;
import '../base_chart.dart' show BaseChart, LifecycleListener;
import '../chart_canvas.dart' show ChartCanvas, getAnimatedColor;
import '../datum_details.dart' show DatumDetails;
import '../processed_series.dart' show ImmutableSeries;
import '../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import 'chart_behavior.dart' show ChartBehavior;
/// Chart behavior that monitors the specified [SelectionModel] and renders a
/// dot for selected data.
///
/// Vertical or horizontal follow lines can optionally be drawn underneath the
/// rendered dots. Follow lines will be drawn in the combined area of the chart
/// draw area, and the draw area for any layout components that provide a
/// series draw area (e.g. [SymbolAnnotationRenderer]).
///
/// This is typically used for line charts to highlight segments.
///
/// It is used in combination with SelectNearest to update the selection model
/// and expand selection out to the domain value.
class LinePointHighlighter<D> implements ChartBehavior<D> {
final SelectionModelType selectionModelType;
/// Default radius of the dots if the series has no radius mapping function.
///
/// When no radius mapping function is provided, this value will be used as
/// is. [radiusPaddingPx] will not be added to [defaultRadiusPx].
final double defaultRadiusPx;
/// Additional radius value added to the radius of the selected data.
///
/// This value is only used when the series has a radius mapping function
/// defined.
final double radiusPaddingPx;
/// Whether or not to draw horizontal follow lines through the selected
/// points.
///
/// Defaults to drawing no horizontal follow lines.
final LinePointHighlighterFollowLineType showHorizontalFollowLine;
/// Whether or not to draw vertical follow lines through the selected points.
///
/// Defaults to drawing a vertical follow line only for the nearest datum.
final LinePointHighlighterFollowLineType showVerticalFollowLine;
/// The dash pattern to be used for drawing the line.
///
/// To disable dash pattern (to draw a solid line), pass in an empty list.
/// This is because if dashPattern is null or not set, it defaults to [1,3].
final List<int> dashPattern;
/// Whether or not follow lines should be drawn across the entire chart draw
/// area, or just from the axis to the point.
///
/// When disabled, measure follow lines will be drawn from the primary measure
/// axis to the point. In RTL mode, this means from the right-hand axis. In
/// LTR mode, from the left-hand axis.
final bool drawFollowLinesAcrossChart;
/// Renderer used to draw the highlighted points.
final SymbolRenderer symbolRenderer;
BaseChart<D> _chart;
_LinePointLayoutView _view;
LifecycleListener<D> _lifecycleListener;
/// Store a map of data drawn on the chart, mapped by series name.
///
/// [LinkedHashMap] is used to render the series on the canvas in the same
/// order as the data was provided by the selection model.
var _seriesPointMap = LinkedHashMap<String, _AnimatedPoint<D>>();
// Store a list of points that exist in the series data.
//
// This list will be used to remove any [_AnimatedPoint] that were rendered in
// previous draw cycles, but no longer have a corresponding datum in the new
// data.
final _currentKeys = <String>[];
LinePointHighlighter(
{SelectionModelType selectionModelType,
double defaultRadiusPx,
double radiusPaddingPx,
LinePointHighlighterFollowLineType showHorizontalFollowLine,
LinePointHighlighterFollowLineType showVerticalFollowLine,
List<int> dashPattern,
bool drawFollowLinesAcrossChart,
SymbolRenderer symbolRenderer})
: selectionModelType = selectionModelType ?? SelectionModelType.info,
defaultRadiusPx = defaultRadiusPx ?? 4.0,
radiusPaddingPx = radiusPaddingPx ?? 2.0,
showHorizontalFollowLine =
showHorizontalFollowLine ?? LinePointHighlighterFollowLineType.none,
showVerticalFollowLine = showVerticalFollowLine ??
LinePointHighlighterFollowLineType.nearest,
dashPattern = dashPattern ?? [1, 3],
drawFollowLinesAcrossChart = drawFollowLinesAcrossChart ?? true,
symbolRenderer = symbolRenderer ?? new CircleSymbolRenderer() {
_lifecycleListener =
new LifecycleListener<D>(onAxisConfigured: _updateViewData);
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
_view = new _LinePointLayoutView<D>(
chart: chart,
layoutPaintOrder: LayoutViewPaintOrder.linePointHighlighter,
showHorizontalFollowLine: showHorizontalFollowLine,
showVerticalFollowLine: showVerticalFollowLine,
dashPattern: dashPattern,
drawFollowLinesAcrossChart: drawFollowLinesAcrossChart,
symbolRenderer: symbolRenderer);
if (chart is CartesianChart) {
// Only vertical rendering is supported by this behavior.
assert((chart as CartesianChart).vertical);
}
chart.addView(_view);
chart.addLifecycleListener(_lifecycleListener);
chart
.getSelectionModel(selectionModelType)
.addSelectionChangedListener(_selectionChanged);
}
@override
void removeFrom(BaseChart chart) {
chart.removeView(_view);
chart
.getSelectionModel(selectionModelType)
.removeSelectionChangedListener(_selectionChanged);
chart.removeLifecycleListener(_lifecycleListener);
}
void _selectionChanged(SelectionModel selectionModel) {
_chart.redraw(skipLayout: true, skipAnimation: true);
}
void _updateViewData() {
_currentKeys.clear();
final selectedDatumDetails =
_chart.getSelectedDatumDetails(selectionModelType);
// Create a new map each time to ensure that we have it sorted in the
// selection model order. This preserves the "nearestDetail" ordering, so
// that we render follow lines in the proper place.
final newSeriesMap = <String, _AnimatedPoint<D>>{};
for (DatumDetails<D> detail in selectedDatumDetails) {
if (detail == null) {
continue;
}
final series = detail.series;
final datum = detail.datum;
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
final lineKey = series.id;
double radiusPx = (detail.radiusPx != null)
? detail.radiusPx.toDouble() + radiusPaddingPx
: defaultRadiusPx;
final pointKey = '${lineKey}::${detail.domain}';
// If we already have a point for that key, use it.
_AnimatedPoint<D> animatingPoint;
if (_seriesPointMap.containsKey(pointKey)) {
animatingPoint = _seriesPointMap[pointKey];
} else {
// Create a new point and have it animate in from axis.
final point = new _DatumPoint<D>(
datum: datum,
domain: detail.domain,
series: series,
x: domainAxis.getLocation(detail.domain),
y: measureAxis.getLocation(0.0));
animatingPoint = new _AnimatedPoint<D>(
key: pointKey, overlaySeries: series.overlaySeries)
..setNewTarget(new _PointRendererElement<D>()
..point = point
..color = detail.color
..fillColor = detail.fillColor
..radiusPx = radiusPx
..measureAxisPosition = measureAxis.getLocation(0.0)
..strokeWidthPx = detail.strokeWidthPx
..symbolRenderer = detail.symbolRenderer);
}
newSeriesMap[pointKey] = animatingPoint;
// Create a new line using the final point locations.
final point = new _DatumPoint<D>(
datum: datum,
domain: detail.domain,
series: series,
x: detail.chartPosition.x,
y: detail.chartPosition.y);
// Update the set of points that still exist in the series data.
_currentKeys.add(pointKey);
// Get the point element we are going to setup.
final pointElement = new _PointRendererElement<D>()
..point = point
..color = detail.color
..fillColor = detail.fillColor
..radiusPx = radiusPx
..measureAxisPosition = measureAxis.getLocation(0.0)
..strokeWidthPx = detail.strokeWidthPx
..symbolRenderer = detail.symbolRenderer;
animatingPoint.setNewTarget(pointElement);
}
// Animate out points that don't exist anymore.
_seriesPointMap.forEach((String key, _AnimatedPoint<D> point) {
if (_currentKeys.contains(point.key) != true) {
point.animateOut();
newSeriesMap[point.key] = point;
}
});
_seriesPointMap = newSeriesMap;
_view.seriesPointMap = _seriesPointMap;
}
@override
String get role => 'LinePointHighlighter-${selectionModelType.toString()}';
}
class _LinePointLayoutView<D> extends LayoutView {
final LayoutViewConfig layoutConfig;
final LinePointHighlighterFollowLineType showHorizontalFollowLine;
final LinePointHighlighterFollowLineType showVerticalFollowLine;
final BaseChart<D> chart;
final List<int> dashPattern;
Rectangle<int> _drawAreaBounds;
Rectangle<int> get drawBounds => _drawAreaBounds;
final bool drawFollowLinesAcrossChart;
final SymbolRenderer symbolRenderer;
GraphicsFactory _graphicsFactory;
/// Store a map of series drawn on the chart, mapped by series name.
///
/// [LinkedHashMap] is used to render the series on the canvas in the same
/// order as the data was given to the chart.
LinkedHashMap<String, _AnimatedPoint<D>> _seriesPointMap;
_LinePointLayoutView({
@required this.chart,
@required int layoutPaintOrder,
@required this.showHorizontalFollowLine,
@required this.showVerticalFollowLine,
@required this.symbolRenderer,
this.dashPattern,
this.drawFollowLinesAcrossChart,
}) : this.layoutConfig = new LayoutViewConfig(
paintOrder: LayoutViewPaintOrder.linePointHighlighter,
position: LayoutPosition.DrawArea,
positionOrder: layoutPaintOrder);
set seriesPointMap(LinkedHashMap<String, _AnimatedPoint<D>> value) {
_seriesPointMap = value;
}
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
return null;
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
this._drawAreaBounds = drawAreaBounds;
}
@override
void paint(ChartCanvas canvas, double animationPercent) {
if (_seriesPointMap == null) {
return;
}
// Clean up the lines that no longer exist.
if (animationPercent == 1.0) {
final keysToRemove = <String>[];
_seriesPointMap.forEach((String key, _AnimatedPoint<D> point) {
if (point.animatingOut) {
keysToRemove.add(key);
}
});
keysToRemove.forEach((String key) => _seriesPointMap.remove(key));
}
final points = <_PointRendererElement<D>>[];
_seriesPointMap.forEach((String key, _AnimatedPoint<D> point) {
points.add(point.getCurrentPoint(animationPercent));
});
// Build maps of the position where the follow lines should stop for each
// selected data point.
final endPointPerValueVertical = <int, int>{};
final endPointPerValueHorizontal = <int, int>{};
for (_PointRendererElement<D> pointElement in points) {
if (pointElement.point.x == null || pointElement.point.y == null) {
continue;
}
final roundedX = pointElement.point.x.round();
final roundedY = pointElement.point.y.round();
// Get the Y value closest to the top of the chart for this X position.
if (endPointPerValueVertical[roundedX] == null) {
endPointPerValueVertical[roundedX] = roundedY;
} else {
// In the nearest case, we rely on the selected data always starting
// with the nearest point. In this case, we don't care about the rest of
// the selected data positions.
if (showVerticalFollowLine !=
LinePointHighlighterFollowLineType.nearest) {
endPointPerValueVertical[roundedX] =
min(endPointPerValueVertical[roundedX], roundedY);
}
}
// Get the X value closest to the "end" side of the chart for this Y
// position.
if (endPointPerValueHorizontal[roundedY] == null) {
endPointPerValueHorizontal[roundedY] = roundedX;
} else {
// In the nearest case, we rely on the selected data always starting
// with the nearest point. In this case, we don't care about the rest of
// the selected data positions.
if (showHorizontalFollowLine !=
LinePointHighlighterFollowLineType.nearest) {
endPointPerValueHorizontal[roundedY] =
max(endPointPerValueHorizontal[roundedY], roundedX);
}
}
}
var shouldShowHorizontalFollowLine = showHorizontalFollowLine ==
LinePointHighlighterFollowLineType.all ||
showHorizontalFollowLine == LinePointHighlighterFollowLineType.nearest;
var shouldShowVerticalFollowLine = showVerticalFollowLine ==
LinePointHighlighterFollowLineType.all ||
showVerticalFollowLine == LinePointHighlighterFollowLineType.nearest;
// Keep track of points for which we've already drawn lines.
final paintedHorizontalLinePositions = <num>[];
final paintedVerticalLinePositions = <num>[];
final drawBounds = chart.drawableLayoutAreaBounds;
final rtl = chart.context.isRtl;
// Draw the follow lines first, below all of the highlight shapes.
for (_PointRendererElement<D> pointElement in points) {
if (pointElement.point.x == null || pointElement.point.y == null) {
continue;
}
final roundedX = pointElement.point.x.round();
final roundedY = pointElement.point.y.round();
// Draw the horizontal follow line.
if (shouldShowHorizontalFollowLine &&
!paintedHorizontalLinePositions.contains(roundedY)) {
int leftBound;
int rightBound;
if (drawFollowLinesAcrossChart) {
// RTL and LTR both go across the whole draw area.
leftBound = drawBounds.left;
rightBound = drawBounds.left + drawBounds.width;
} else {
final x = endPointPerValueHorizontal[roundedY];
// RTL goes from the point to the right edge. LTR goes from the left
// edge to the point.
leftBound = rtl ? x : drawBounds.left;
rightBound = rtl ? drawBounds.left + drawBounds.width : x;
}
canvas.drawLine(
points: [
new Point<num>(leftBound, pointElement.point.y),
new Point<num>(rightBound, pointElement.point.y),
],
stroke: StyleFactory.style.linePointHighlighterColor,
strokeWidthPx: 1.0,
dashPattern: [1, 3]);
if (showHorizontalFollowLine ==
LinePointHighlighterFollowLineType.nearest) {
shouldShowHorizontalFollowLine = false;
}
paintedHorizontalLinePositions.add(roundedY);
}
// Draw the vertical follow line.
if (shouldShowVerticalFollowLine &&
!paintedVerticalLinePositions.contains(roundedX)) {
final topBound = drawFollowLinesAcrossChart
? drawBounds.top
: endPointPerValueVertical[roundedX];
canvas.drawLine(
points: [
new Point<num>(pointElement.point.x, topBound),
new Point<num>(
pointElement.point.x, drawBounds.top + drawBounds.height),
],
stroke: StyleFactory.style.linePointHighlighterColor,
strokeWidthPx: 1.0,
dashPattern: dashPattern);
if (showVerticalFollowLine ==
LinePointHighlighterFollowLineType.nearest) {
shouldShowVerticalFollowLine = false;
}
paintedVerticalLinePositions.add(roundedX);
}
if (!shouldShowHorizontalFollowLine && !shouldShowVerticalFollowLine) {
break;
}
}
// Draw the highlight shapes on top of all follow lines.
for (_PointRendererElement<D> pointElement in points) {
if (pointElement.point.x == null || pointElement.point.y == null) {
continue;
}
final bounds = new Rectangle<double>(
pointElement.point.x - pointElement.radiusPx,
pointElement.point.y - pointElement.radiusPx,
pointElement.radiusPx * 2,
pointElement.radiusPx * 2);
// Draw the highlight dot. Use the [SymbolRenderer] from the datum if one
// is defined.
(pointElement.symbolRenderer ?? symbolRenderer).paint(canvas, bounds,
fillColor: pointElement.fillColor,
strokeColor: pointElement.color,
strokeWidthPx: pointElement.strokeWidthPx);
}
}
@override
Rectangle<int> get componentBounds => this._drawAreaBounds;
@override
bool get isSeriesRenderer => false;
}
class _DatumPoint<D> extends Point<double> {
final dynamic datum;
final D domain;
final ImmutableSeries<D> series;
_DatumPoint({this.datum, this.domain, this.series, double x, double y})
: super(x, y);
factory _DatumPoint.from(_DatumPoint<D> other, [double x, double y]) {
return new _DatumPoint<D>(
datum: other.datum,
domain: other.domain,
series: other.series,
x: x ?? other.x,
y: y ?? other.y);
}
}
class _PointRendererElement<D> {
_DatumPoint<D> point;
Color color;
Color fillColor;
double radiusPx;
double measureAxisPosition;
double strokeWidthPx;
SymbolRenderer symbolRenderer;
_PointRendererElement<D> clone() {
return new _PointRendererElement<D>()
..point = this.point
..color = this.color
..fillColor = this.fillColor
..measureAxisPosition = this.measureAxisPosition
..radiusPx = this.radiusPx
..strokeWidthPx = this.strokeWidthPx
..symbolRenderer = this.symbolRenderer;
}
void updateAnimationPercent(_PointRendererElement previous,
_PointRendererElement target, double animationPercent) {
final targetPoint = target.point;
final previousPoint = previous.point;
final x = _lerpDouble(previousPoint.x, targetPoint.x, animationPercent);
final y = _lerpDouble(previousPoint.y, targetPoint.y, animationPercent);
point = new _DatumPoint<D>.from(targetPoint, x, y);
color = getAnimatedColor(previous.color, target.color, animationPercent);
fillColor = getAnimatedColor(
previous.fillColor, target.fillColor, animationPercent);
radiusPx =
_lerpDouble(previous.radiusPx, target.radiusPx, animationPercent);
if (target.strokeWidthPx != null && previous.strokeWidthPx != null) {
strokeWidthPx = (((target.strokeWidthPx - previous.strokeWidthPx) *
animationPercent) +
previous.strokeWidthPx);
} else {
strokeWidthPx = null;
}
}
/// Linear interpolation for doubles.
///
/// If either [a] or [b] is null, return null.
/// This is different than Flutter's lerpDouble method, we want to return null
/// instead of assuming it is 0.0.
double _lerpDouble(double a, double b, double t) {
if (a == null || b == null) return null;
return a + (b - a) * t;
}
}
class _AnimatedPoint<D> {
final String key;
final bool overlaySeries;
_PointRendererElement<D> _previousPoint;
_PointRendererElement<D> _targetPoint;
_PointRendererElement<D> _currentPoint;
// Flag indicating whether this point is being animated out of the chart.
bool animatingOut = false;
_AnimatedPoint({@required this.key, @required this.overlaySeries});
/// Animates a point that was removed from the series out of the view.
///
/// This should be called in place of "setNewTarget" for points that represent
/// data that has been removed from the series.
///
/// Animates the height of the point down to the measure axis position
/// (position of 0).
void animateOut() {
final newTarget = _currentPoint.clone();
// Set the target measure value to the axis position for all points.
final targetPoint = newTarget.point;
final newPoint = new _DatumPoint<D>.from(targetPoint, targetPoint.x,
newTarget.measureAxisPosition.roundToDouble());
newTarget.point = newPoint;
// Animate the radius to 0 so that we don't get a lingering point after
// animation is done.
newTarget.radiusPx = 0.0;
setNewTarget(newTarget);
animatingOut = true;
}
void setNewTarget(_PointRendererElement<D> newTarget) {
animatingOut = false;
_currentPoint ??= newTarget.clone();
_previousPoint = _currentPoint.clone();
_targetPoint = newTarget;
}
_PointRendererElement<D> getCurrentPoint(double animationPercent) {
if (animationPercent == 1.0 || _previousPoint == null) {
_currentPoint = _targetPoint;
_previousPoint = _targetPoint;
return _currentPoint;
}
_currentPoint.updateAnimationPercent(
_previousPoint, _targetPoint, animationPercent);
return _currentPoint;
}
}
/// Type of follow line(s) to draw.
enum LinePointHighlighterFollowLineType {
/// Draw a follow line for only the nearest point in the selection.
nearest,
/// Draw no follow lines.
none,
/// Draw a follow line for every point in the selection.
all,
}
/// Helper class that exposes fewer private internal properties for unit tests.
@visibleForTesting
class LinePointHighlighterTester<D> {
final LinePointHighlighter<D> behavior;
LinePointHighlighterTester(this.behavior);
int getSelectionLength() {
return behavior._seriesPointMap.length;
}
bool isDatumSelected(D datum) {
var contains = false;
behavior._seriesPointMap.forEach((String key, _AnimatedPoint<D> point) {
if (point._currentPoint.point.datum == datum) {
contains = true;
return;
}
});
return contains;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,127 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import '../../../../common/gesture_listener.dart' show GestureListener;
import '../../base_chart.dart' show BaseChart;
import '../../behavior/chart_behavior.dart' show ChartBehavior;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import 'selection_trigger.dart' show SelectionTrigger;
/// Chart behavior that listens to tap event trigges and locks the specified
/// [SelectionModel]. This is used to prevent further updates to the selection
/// model, until it is unlocked again.
///
/// SelectionModels that can be updated:
/// info - To view the details of the selected items (ie: hover for web).
/// action - To select an item as an input, drill, or other selection.
///
/// You can add one LockSelection for each model type that you are updating.
/// Any previous LockSelection behavior for that selection model will be
/// removed.
class LockSelection<D> implements ChartBehavior<D> {
GestureListener _listener;
/// Type of selection model that should be updated by input events.
final SelectionModelType selectionModelType;
/// Type of input event that should trigger selection.
final SelectionTrigger eventTrigger = SelectionTrigger.tap;
BaseChart<D> _chart;
LockSelection({this.selectionModelType = SelectionModelType.info}) {
// Setup the appropriate gesture listening.
switch (this.eventTrigger) {
case SelectionTrigger.tap:
_listener =
new GestureListener(onTapTest: _onTapTest, onTap: _onSelect);
break;
default:
throw new ArgumentError('LockSelection does not support the event '
'trigger "${this.eventTrigger}"');
break;
}
}
bool _onTapTest(Point<double> chartPoint) {
// If the tap is within the drawArea, then claim the event from others.
return _chart.pointWithinRenderer(chartPoint);
}
bool _onSelect(Point<double> chartPoint, [double ignored]) {
// Skip events that occur outside the drawArea for any series renderer.
if (!_chart.pointWithinRenderer(chartPoint)) {
return false;
}
final selectionModel = _chart.getSelectionModel(selectionModelType);
// Do nothing if the chart has no selection model.
if (selectionModel == null) {
return false;
}
// Do not lock the selection model if there is no selection. Locking nothing
// would result in a very confusing user interface as the user tries to
// interact with content on the chart.
if (!selectionModel.locked && !selectionModel.hasAnySelection) {
return false;
}
// Toggle the lock state.
selectionModel.locked = !selectionModel.locked;
// If the model was just unlocked, clear the selection to dismiss any stale
// behavior elements. A new hovercard/etc. will appear after the user
// triggers a new gesture.
if (!selectionModel.locked) {
selectionModel.clearSelection();
}
return false;
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addGestureListener(_listener);
// TODO: Update this dynamically based on tappable location.
switch (this.eventTrigger) {
case SelectionTrigger.tap:
case SelectionTrigger.tapAndDrag:
case SelectionTrigger.pressHold:
case SelectionTrigger.longPressHold:
chart.registerTappable(this);
break;
case SelectionTrigger.hover:
default:
chart.unregisterTappable(this);
break;
}
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeGestureListener(_listener);
chart.unregisterTappable(this);
_chart = null;
}
@override
String get role => 'LockSelection-${selectionModelType.toString()}}';
}

View File

@@ -0,0 +1,302 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import '../../../../common/gesture_listener.dart' show GestureListener;
import '../../base_chart.dart' show BaseChart;
import '../../behavior/chart_behavior.dart' show ChartBehavior;
import '../../datum_details.dart' show DatumDetails;
import '../../processed_series.dart' show ImmutableSeries;
import '../../selection_model/selection_model.dart' show SelectionModelType;
import '../../series_datum.dart' show SeriesDatum;
import 'selection_trigger.dart' show SelectionTrigger;
/// Chart behavior that listens to the given eventTrigger and updates the
/// specified [SelectionModel]. This is used to pair input events to behaviors
/// that listen to selection changes.
///
/// Input event types:
/// hover (default) - Mouse over/near data.
/// tap - Mouse/Touch on/near data.
/// pressHold - Mouse/Touch and drag across the data instead of panning.
/// longPressHold - Mouse/Touch for a while in one place then drag across the
/// data.
///
/// SelectionModels that can be updated:
/// info - To view the details of the selected items (ie: hover for web).
/// action - To select an item as an input, drill, or other selection.
///
/// Other options available
/// [expandToDomain] - All data points that match the domain value of the
/// closest data point from each Series will be included in the selection.
/// The selection is limited to the hovered component area unless
/// [selectAcrossAllSeriesRendererComponents] is set to true. (Default:
/// true)
/// [selectAcrossAllSeriesRendererComponents] - Events in any component that
/// draw Series data will propagate to other components that draw Series
/// data to get a union of points that match across all series renderer
/// components. This is useful when components in the margins draw series
/// data and a selection is supposed to bridge the two adjacent
/// components. (Default: true)
/// [selectClosestSeries] - If true, the closest Series itself will be marked
/// as selected in addition to the datum. This is useful for features like
/// highlighting the closest Series. (Default: true)
///
/// You can add one SelectNearest for each model type that you are updating.
/// Any previous SelectNearest behavior for that selection model will be
/// removed.
class SelectNearest<D> implements ChartBehavior<D> {
GestureListener _listener;
/// Type of selection model that should be updated by input events.
final SelectionModelType selectionModelType;
/// Type of input event that should trigger selection.
final SelectionTrigger eventTrigger;
/// Whether or not all data points that match the domain value of the closest
/// data point from each Series will be included in the selection.
///
/// The selection is limited to the hovered component area unless
/// [selectAcrossAllSeriesRendererComponents] is set to true.
final bool expandToDomain;
/// Whether or not events in any component that draw Series data will
/// propagate to other components that draw Series data to get a union of
/// points that match across all series renderer components.
///
/// This is useful when components in the margins draw series data and a
/// selection is supposed to bridge the two adjacent components.
final bool selectAcrossAllSeriesRendererComponents;
/// Whether or not the closest Series itself will be marked as selected in
/// addition to the datum.
final bool selectClosestSeries;
/// The farthest away a domain value can be from the mouse position on the
/// domain axis before we'll ignore the datum.
///
/// This allows sparse data to not get selected until the mouse is some
/// reasonable distance. Defaults to no maximum distance.
final int maximumDomainDistancePx;
BaseChart<D> _chart;
bool _delaySelect = false;
SelectNearest(
{this.selectionModelType = SelectionModelType.info,
this.expandToDomain = true,
this.selectAcrossAllSeriesRendererComponents = true,
this.selectClosestSeries = true,
this.eventTrigger = SelectionTrigger.hover,
this.maximumDomainDistancePx}) {
// Setup the appropriate gesture listening.
switch (this.eventTrigger) {
case SelectionTrigger.tap:
_listener =
new GestureListener(onTapTest: _onTapTest, onTap: _onSelect);
break;
case SelectionTrigger.tapAndDrag:
_listener = new GestureListener(
onTapTest: _onTapTest,
onTap: _onSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
);
break;
case SelectionTrigger.pressHold:
_listener = new GestureListener(
onTapTest: _onTapTest,
onLongPress: _onSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDeselectAll);
break;
case SelectionTrigger.longPressHold:
_listener = new GestureListener(
onTapTest: _onTapTest,
onLongPress: _onLongPressSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDeselectAll);
break;
case SelectionTrigger.hover:
default:
_listener = new GestureListener(onHover: _onSelect);
break;
}
}
bool _onTapTest(Point<double> chartPoint) {
// If the tap is within the drawArea, then claim the event from others.
_delaySelect = eventTrigger == SelectionTrigger.longPressHold;
return _chart.pointWithinRenderer(chartPoint);
}
bool _onLongPressSelect(Point<double> chartPoint) {
_delaySelect = false;
return _onSelect(chartPoint);
}
bool _onSelect(Point<double> chartPoint, [double ignored]) {
// If the selection is delayed (waiting for long press), then quit early.
if (_delaySelect) {
return false;
}
var details = _chart.getNearestDatumDetailPerSeries(
chartPoint, selectAcrossAllSeriesRendererComponents);
final seriesList = <ImmutableSeries<D>>[];
var seriesDatumList = <SeriesDatum<D>>[];
if (details != null && details.isNotEmpty) {
details.sort((a, b) => a.domainDistance.compareTo(b.domainDistance));
if (maximumDomainDistancePx == null ||
details[0].domainDistance <= maximumDomainDistancePx) {
seriesDatumList = expandToDomain
? _expandToDomain(details.first)
: [new SeriesDatum<D>(details.first.series, details.first.datum)];
// Filter out points from overlay series.
seriesDatumList
.removeWhere((SeriesDatum<D> datum) => datum.series.overlaySeries);
if (selectClosestSeries && seriesList.isEmpty) {
if (details.first.series.overlaySeries) {
// If the closest "details" was from an overlay series, grab the
// closest remaining series instead. In this case, we need to sort a
// copy of the list by domain distance because we do not want to
// re-order the actual return values here.
final sortedSeriesDatumList =
new List<SeriesDatum<D>>.from(seriesDatumList);
sortedSeriesDatumList.sort((a, b) =>
a.datum.domainDistance.compareTo(b.datum.domainDistance));
seriesList.add(sortedSeriesDatumList.first.series);
} else {
seriesList.add(details.first.series);
}
}
}
}
return _chart
.getSelectionModel(selectionModelType)
.updateSelection(seriesDatumList, seriesList);
}
bool _onDeselectAll(_, __, ___) {
// If the selection is delayed (waiting for long press), then quit early.
if (_delaySelect) {
return false;
}
_chart
.getSelectionModel(selectionModelType)
.updateSelection(<SeriesDatum<D>>[], <ImmutableSeries<D>>[]);
return false;
}
List<SeriesDatum<D>> _expandToDomain(DatumDetails<D> nearestDetails) {
// Make sure that the "nearest" datum is at the top of the list.
final data = <SeriesDatum<D>>[
new SeriesDatum(nearestDetails.series, nearestDetails.datum)
];
final nearestDomain = nearestDetails.domain;
for (ImmutableSeries<D> series in _chart.currentSeriesList) {
final domainFn = series.domainFn;
final domainLowerBoundFn = series.domainLowerBoundFn;
final domainUpperBoundFn = series.domainUpperBoundFn;
final testBounds =
domainLowerBoundFn != null && domainUpperBoundFn != null;
for (var i = 0; i < series.data.length; i++) {
final datum = series.data[i];
final domain = domainFn(i);
// Don't re-add the nearest details.
if (nearestDetails.series == series && nearestDetails.datum == datum) {
continue;
}
if (domain == nearestDomain) {
data.add(new SeriesDatum(series, datum));
} else if (testBounds) {
final domainLowerBound = domainLowerBoundFn(i);
final domainUpperBound = domainUpperBoundFn(i);
var addDatum = false;
if (domainLowerBound != null && domainUpperBound != null) {
if (domain is int) {
addDatum = (domainLowerBound as int) <= (nearestDomain as int) &&
(nearestDomain as int) <= (domainUpperBound as int);
} else if (domain is double) {
addDatum =
(domainLowerBound as double) <= (nearestDomain as double) &&
(nearestDomain as double) <= (domainUpperBound as double);
} else if (domain is DateTime) {
addDatum = domainLowerBound == nearestDomain ||
domainUpperBound == nearestDomain ||
((domainLowerBound as DateTime)
.isBefore(nearestDomain as DateTime) &&
(nearestDomain as DateTime)
.isBefore(domainUpperBound as DateTime));
}
}
if (addDatum) {
data.add(new SeriesDatum(series, datum));
}
}
}
}
return data;
}
@override
void attachTo(BaseChart<D> chart) {
_chart = chart;
chart.addGestureListener(_listener);
// TODO: Update this dynamically based on tappable location.
switch (this.eventTrigger) {
case SelectionTrigger.tap:
case SelectionTrigger.tapAndDrag:
case SelectionTrigger.pressHold:
case SelectionTrigger.longPressHold:
chart.registerTappable(this);
break;
case SelectionTrigger.hover:
default:
chart.unregisterTappable(this);
break;
}
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeGestureListener(_listener);
chart.unregisterTappable(this);
_chart = null;
}
@override
String get role => 'SelectNearest-${selectionModelType.toString()}}';
}

View File

@@ -0,0 +1,22 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
enum SelectionTrigger {
hover,
tap,
tapAndDrag,
pressHold,
longPressHold,
}

View File

@@ -0,0 +1,816 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math';
import 'package:meta/meta.dart';
import '../../../../common/color.dart' show Color;
import '../../../../common/gesture_listener.dart' show GestureListener;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../../common/math.dart' show clamp;
import '../../../../common/style/style_factory.dart' show StyleFactory;
import '../../../../common/symbol_renderer.dart'
show RectSymbolRenderer, SymbolRenderer;
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../../../layout/layout_view.dart'
show
LayoutPosition,
LayoutView,
LayoutViewConfig,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
ViewMeasuredSizes;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../../behavior/chart_behavior.dart' show ChartBehavior;
import '../../chart_canvas.dart' show ChartCanvas, getAnimatedColor;
import '../selection/selection_trigger.dart' show SelectionTrigger;
/// Chart behavior that adds a slider widget to a chart. When the slider is
/// dropped after drag, it will report its domain position and nearest datum
/// value. This behavior only supports charts that use continuous scales.
///
/// Input event types:
/// tapAndDrag - Mouse/Touch on the handle and drag across the chart.
/// pressHold - Mouse/Touch on the handle and drag across the chart instead of
/// panning.
/// longPressHold - Mouse/Touch for a while on the handle, then drag across
/// the data.
class Slider<D> implements ChartBehavior<D> {
_SliderLayoutView _view;
GestureListener _gestureListener;
LifecycleListener<D> _lifecycleListener;
SliderEventListener<D> _sliderEventListener;
/// The order to paint slider on the canvas.
///
/// The smaller number is drawn first. This value should be relative to
/// LayoutPaintViewOrder.slider (e.g. LayoutViewPaintOrder.slider + 1).
int layoutPaintOrder;
/// Type of input event for the slider.
///
/// Input event types:
/// tapAndDrag - Mouse/Touch on the handle and drag across the chart.
/// pressHold - Mouse/Touch on the handle and drag across the chart instead
/// of panning.
/// longPressHold - Mouse/Touch for a while on the handle, then drag across
/// the data.
final SelectionTrigger eventTrigger;
/// Renderer for the handle. Defaults to a rectangle.
SymbolRenderer _handleRenderer;
/// Custom role ID for this slider
String _roleId;
/// Whether or not the slider will snap onto the nearest datum (by domain
/// distance) when dragged.
final bool snapToDatum;
/// Color and size styles for the slider.
SliderStyle _style;
CartesianChart<D> _chart;
/// Rendering data for the slider line and handle.
_AnimatedSlider _sliderHandle;
bool _delaySelect = false;
bool _handleDrag = false;
/// Current location of the slider line.
Point<int> _domainCenterPoint;
/// Previous location of the slider line.
///
/// This is used to track changes in the position of the slider caused by new
/// data being drawn on the chart.
Point<int> _previousDomainCenterPoint;
/// Bounding box for the slider drag handle.
Rectangle<int> _handleBounds;
/// Domain value of the current slider position.
///
/// This is saved in terms of domain instead of chart position so that we can
/// adjust the slider automatically when the chart is resized.
D _domainValue;
/// Event to fire during the chart's onPostrender event.
///
/// This should be set any time the state of the slider has changed.
SliderListenerDragState _dragStateToFireOnPostRender;
/// Constructs a [Slider].
///
/// [eventTrigger] sets the type of gesture handled by the slider.
///
/// [handleRenderer] draws a handle for the slider. Defaults to a rectangle.
///
/// [initialDomainValue] sets the initial position of the slider in domain
/// units. The default is the center of the chart.
///
/// [onChangeCallback] will be called when the position of the slider
/// changes during a drag event.
///
/// [roleId] optional custom role ID for the slider. This can be used to allow
/// multiple [Slider] behaviors on the same chart. Normally, there can only be
/// one slider (per event trigger type) on a chart. This setting allows for
/// configuring multiple independent sliders.
///
/// [snapToDatum] configures the slider to snap snap onto the nearest datum
/// (by domain distance) when dragged. By default, the slider can be
/// positioned anywhere along the domain axis.
///
/// [style] configures the color and sizing of the slider line and handle.
///
/// [layoutPaintOrder] configures the order in which the behavior should be
/// painted. This value should be relative to LayoutPaintViewOrder.slider.
/// (e.g. LayoutViewPaintOrder.slider + 1).
Slider(
{this.eventTrigger = SelectionTrigger.tapAndDrag,
SymbolRenderer handleRenderer,
D initialDomainValue,
SliderListenerCallback<D> onChangeCallback,
String roleId,
this.snapToDatum = false,
SliderStyle style,
this.layoutPaintOrder = LayoutViewPaintOrder.slider}) {
_handleRenderer = handleRenderer ?? new RectSymbolRenderer();
_roleId = roleId ?? '';
_style = style ?? new SliderStyle();
_domainValue = initialDomainValue;
if (_domainValue != null) {
_dragStateToFireOnPostRender = SliderListenerDragState.initial;
}
// Setup the appropriate gesture listening.
switch (this.eventTrigger) {
case SelectionTrigger.tapAndDrag:
_gestureListener = new GestureListener(
onTapTest: _onTapTest,
onTap: _onSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDragEnd);
break;
case SelectionTrigger.pressHold:
_gestureListener = new GestureListener(
onTapTest: _onTapTest,
onLongPress: _onSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDragEnd);
break;
case SelectionTrigger.longPressHold:
_gestureListener = new GestureListener(
onTapTest: _onTapTest,
onLongPress: _onLongPressSelect,
onDragStart: _onSelect,
onDragUpdate: _onSelect,
onDragEnd: _onDragEnd);
break;
default:
throw new ArgumentError('Slider does not support the event trigger '
'"${this.eventTrigger}"');
break;
}
// Set up chart draw cycle listeners.
_lifecycleListener = new LifecycleListener<D>(
onData: _setInitialDragState,
onAxisConfigured: _updateViewData,
onPostrender: _fireChangeEvent,
);
// Set up slider event listeners.
_sliderEventListener =
new SliderEventListener<D>(onChange: onChangeCallback);
}
bool _onTapTest(Point<double> chartPoint) {
_delaySelect = eventTrigger == SelectionTrigger.longPressHold;
_handleDrag = _sliderContainsPoint(chartPoint);
return _handleDrag;
}
bool _onLongPressSelect(Point<double> chartPoint) {
_delaySelect = false;
return _onSelect(chartPoint);
}
bool _onSelect(Point<double> chartPoint, [double ignored]) {
// Skip events that occur outside the drawArea for any series renderer.
// If the selection is delayed (waiting for long press), then quit early.
if (!_handleDrag || _delaySelect) {
return false;
}
// Move the slider line along the domain axis, without adjusting the measure
// position.
final positionChanged = _moveSliderToPoint(chartPoint);
if (positionChanged) {
_dragStateToFireOnPostRender = SliderListenerDragState.drag;
_chart.redraw(skipAnimation: true, skipLayout: true);
}
return true;
}
bool _onDragEnd(Point<double> chartPoint, __, ___) {
// If the selection is delayed (waiting for long press), then quit early.
if (_delaySelect) {
return false;
}
_handleDrag = false;
// If snapToDatum is enabled, use the x position of the nearest datum
// instead of the mouse point.
if (snapToDatum) {
final details = _chart.getNearestDatumDetailPerSeries(chartPoint, true);
if (details.isNotEmpty && details[0].chartPosition.x != null) {
// Only trigger an animating draw cycle if we need to move the slider.
if (_domainValue != details[0].domain) {
_moveSliderToDomain(details[0].domain);
// Always fire the end event to notify listeners that the gesture is
// over.
_dragStateToFireOnPostRender = SliderListenerDragState.end;
_chart.redraw(skipAnimation: false, skipLayout: true);
}
}
} else {
// Move the slider line along the domain axis, without adjusting the
// measure position.
_moveSliderToPoint(chartPoint);
// Always fire the end event to notify listeners that the gesture is
// over.
_dragStateToFireOnPostRender = SliderListenerDragState.end;
_chart.redraw(skipAnimation: true, skipLayout: true);
}
return false;
}
bool _sliderContainsPoint(Point<double> chartPoint) {
return _handleBounds.containsPoint(chartPoint);
}
/// Sets the drag state to "initial" when new data is drawn on the chart.
void _setInitialDragState(_) {
_dragStateToFireOnPostRender = SliderListenerDragState.initial;
}
void _updateViewData() {
_sliderHandle ??= new _AnimatedSlider();
// If not set in the constructor, initial position for the handle is the
// center of the draw area.
_domainValue ??= _chart.domainAxis
.getDomain(_view.drawBounds.left + _view.drawBounds.width / 2)
.round();
// Possibly move the slider, if the axis values have changed since the last
// chart draw.
_moveSliderToDomain(_domainValue);
// Move the handle to the current event position.
final element = new _SliderElement()
..domainCenterPoint =
new Point<int>(_domainCenterPoint.x, _domainCenterPoint.y)
..buttonBounds = new Rectangle<int>(_handleBounds.left, _handleBounds.top,
_handleBounds.width, _handleBounds.height)
..fill = _style.fillColor
..stroke = _style.strokeColor
..strokeWidthPx = _style.strokeWidthPx;
_sliderHandle.setNewTarget(element);
_view.sliderHandle = _sliderHandle;
}
/// Fires a [SliderListenerDragState] change event if needed.
void _fireChangeEvent(_) {
if (SliderListenerDragState == null ||
_sliderEventListener.onChange == null) {
return;
}
SliderListenerDragState dragState = _dragStateToFireOnPostRender;
// Initial drag state event should only be fired if the slider has moved
// since the last draw. We always set the initial drag state event when new
// data was drawn on the chart, since we might need to move the slider if
// the axis range changed.
if (dragState == SliderListenerDragState.initial &&
_previousDomainCenterPoint == _domainCenterPoint) {
dragState = null;
}
// Reset state.
_dragStateToFireOnPostRender = null;
_previousDomainCenterPoint = _domainCenterPoint;
// Bail out if the event was cancelled.
if (dragState == null) {
return;
}
// Fire the event.
_sliderEventListener.onChange(
new Point<int>(_domainCenterPoint.x, _domainCenterPoint.y),
_domainValue,
_roleId,
dragState);
}
/// Moves the slider along the domain axis to [point].
///
/// If [point] exists beyond either edge of the draw area, it will be bound to
/// the nearest edge.
///
/// Updates [_domainValue] with the domain value located at [point]. For
/// ordinal axes, this might technically result in a domain value whose center
/// point lies slightly outside the draw area.
///
/// Updates [_domainCenterPoint] and [_handleBounds] with the new position of
/// the slider.
///
/// Returns whether or not the position actually changed. This will generally
/// be false if the mouse was dragged outside of the domain axis viewport.
bool _moveSliderToPoint(Point<double> point) {
var positionChanged = false;
if (_chart != null) {
final viewBounds = _view.componentBounds;
// Clamp the position to the edge of the viewport.
final position = clamp(point.x, viewBounds.left, viewBounds.right);
positionChanged = (_previousDomainCenterPoint != null &&
position != _previousDomainCenterPoint.x);
// Reset the domain value if the position was outside of the chart.
_domainValue = _chart.domainAxis.getDomain(position.toDouble());
if (_domainCenterPoint != null) {
_domainCenterPoint =
new Point<int>(position.round(), _domainCenterPoint.y);
} else {
_domainCenterPoint = new Point<int>(
position.round(), (viewBounds.top + viewBounds.height / 2).round());
}
num handleReferenceY;
switch (_style.handlePosition) {
case SliderHandlePosition.middle:
handleReferenceY = _domainCenterPoint.y;
break;
case SliderHandlePosition.top:
handleReferenceY = viewBounds.top;
break;
default:
throw new ArgumentError('Slider does not support the handle position '
'"${_style.handlePosition}"');
}
// Move the slider handle along the domain axis.
_handleBounds = new Rectangle<int>(
(_domainCenterPoint.x -
_style.handleSize.width / 2 +
_style.handleOffset.x)
.round(),
(handleReferenceY -
_style.handleSize.height / 2 +
_style.handleOffset.y)
.round(),
_style.handleSize.width,
_style.handleSize.height);
}
return positionChanged;
}
/// Moves the slider along the domain axis to the location of [domain].
///
/// If [domain] exists beyond either edge of the draw area, the position will
/// be bound to the nearest edge.
///
/// Updates [_domainValue] with the location of [domain]. For ordinal axes,
/// this might result in a different domain value if the range band of
/// [domain] is completely outside of the viewport.
///
/// Updates [_domainCenterPoint] and [_handleBounds] with the new position of
/// the slider.
///
/// Returns whether or not the position actually changed. This will generally
/// be false if the mouse was dragged outside of the domain axis viewport.
bool _moveSliderToDomain(D domain) {
final x = _chart.domainAxis.getLocation(domain);
return _moveSliderToPoint(new Point<double>(x, 0.0));
}
/// Programmatically moves the slider to the location of [domain] on the
/// domain axis.
///
/// If [domain] exists beyond either edge of the draw area, the position will
/// be bound to the nearest edge of the chart. The slider's current domain
/// value state will reflect the domain value at the edge of the chart. For
/// ordinal axes, this might result in a domain value whose range band is
/// partially located beyond the edge of the chart.
///
/// This does nothing if the domain matches the current domain location.
///
/// [SliderEventListener] callbacks will be fired to indicate that the slider
/// has moved.
///
/// [skipAnimation] controls whether or not the slider will animate. Animation
/// is disabled by default.
void moveSliderToDomain(D domain, {bool skipAnimation = true}) {
// Nothing to do if we are unattached to a chart or asked to move to the
// current location.
if (_chart == null || domain == _domainValue) {
return;
}
final positionChanged = _moveSliderToDomain(domain);
if (positionChanged) {
_dragStateToFireOnPostRender = SliderListenerDragState.end;
_chart.redraw(skipAnimation: skipAnimation, skipLayout: true);
}
}
@override
void attachTo(BaseChart<D> chart) {
if (!(chart is CartesianChart)) {
throw new ArgumentError(
'Slider can only be attached to a cartesian chart.');
}
_chart = chart as CartesianChart;
// Only vertical rendering is supported by this behavior.
assert(_chart.vertical);
_view = new _SliderLayoutView<D>(
layoutPaintOrder: layoutPaintOrder, handleRenderer: _handleRenderer);
chart.addView(_view);
chart.addGestureListener(_gestureListener);
chart.addLifecycleListener(_lifecycleListener);
}
@override
void removeFrom(BaseChart<D> chart) {
chart.removeView(_view);
chart.removeGestureListener(_gestureListener);
chart.removeLifecycleListener(_lifecycleListener);
_chart = null;
}
@override
String get role => 'Slider-${eventTrigger.toString()}-${_roleId}';
}
/// Style configuration for a [Slider] behavior.
class SliderStyle {
/// Fill color of the handle of the slider.
Color fillColor;
/// Allows users to specify both x-position and y-position offset values that
/// determines where the slider handle will be rendered. The offset will be
/// calculated relative to its default position at the vertical and horizontal
/// center of the slider line.
Point<double> handleOffset;
/// The vertical position for the slider handle.
SliderHandlePosition handlePosition;
/// Specifies the size of the slider handle.
Rectangle<int> handleSize;
/// Stroke width of the slider line and the slider handle.
double strokeWidthPx;
/// Stroke color of the slider line and hte slider handle
Color strokeColor = StyleFactory.style.sliderStrokeColor;
SliderStyle(
{Color fillColor,
this.handleOffset = const Point<double>(0.0, 0.0),
this.handleSize = const Rectangle<int>(0, 0, 10, 20),
Color strokeColor,
this.handlePosition = SliderHandlePosition.middle,
this.strokeWidthPx = 2.0}) {
this.fillColor = fillColor ?? StyleFactory.style.sliderFillColor;
this.strokeColor = strokeColor ?? StyleFactory.style.sliderStrokeColor;
}
@override
bool operator ==(Object o) {
return o is SliderStyle &&
fillColor == o.fillColor &&
handleOffset == o.handleOffset &&
handleSize == o.handleSize &&
strokeWidthPx == o.strokeWidthPx &&
strokeColor == o.strokeColor;
}
@override
int get hashCode {
int hashcode = fillColor?.hashCode ?? 0;
hashcode = (hashcode * 37) + handleOffset?.hashCode ?? 0;
hashcode = (hashcode * 37) + handleSize?.hashCode ?? 0;
hashcode = (hashcode * 37) + strokeWidthPx?.hashCode ?? 0;
hashcode = (hashcode * 37) + strokeColor?.hashCode ?? 0;
hashcode = (hashcode * 37) + handlePosition?.hashCode ?? 0;
return hashcode;
}
}
/// Describes the vertical position of the slider handle on the slider.
///
/// [middle] indicates the handle should be half-way between the top and bottom
/// of the chart in the middle of the slider line.
///
/// [top] indicates the slider should be rendered relative to the top of the
/// chart.
enum SliderHandlePosition { middle, top }
/// Layout view component for [Slider].
class _SliderLayoutView<D> extends LayoutView {
final LayoutViewConfig layoutConfig;
Rectangle<int> _drawAreaBounds;
Rectangle<int> get drawBounds => _drawAreaBounds;
GraphicsFactory _graphicsFactory;
/// Renderer for the handle. Defaults to a rectangle.
SymbolRenderer _handleRenderer;
/// Rendering data for the slider line and handle.
_AnimatedSlider _sliderHandle;
_SliderLayoutView(
{@required int layoutPaintOrder, @required SymbolRenderer handleRenderer})
: this.layoutConfig = new LayoutViewConfig(
paintOrder: layoutPaintOrder,
position: LayoutPosition.DrawArea,
positionOrder: LayoutViewPositionOrder.drawArea),
_handleRenderer = handleRenderer;
set sliderHandle(_AnimatedSlider value) {
_sliderHandle = value;
}
@override
GraphicsFactory get graphicsFactory => _graphicsFactory;
@override
set graphicsFactory(GraphicsFactory value) {
_graphicsFactory = value;
}
@override
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
return null;
}
@override
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
this._drawAreaBounds = drawAreaBounds;
}
@override
void paint(ChartCanvas canvas, double animationPercent) {
final sliderElement = _sliderHandle.getCurrentSlider(animationPercent);
canvas.drawLine(
points: [
new Point<num>(
sliderElement.domainCenterPoint.x, _drawAreaBounds.top),
new Point<num>(
sliderElement.domainCenterPoint.x, _drawAreaBounds.bottom),
],
stroke: sliderElement.stroke,
strokeWidthPx: sliderElement.strokeWidthPx);
_handleRenderer.paint(canvas, sliderElement.buttonBounds,
fillColor: sliderElement.fill,
strokeColor: sliderElement.stroke,
strokeWidthPx: sliderElement.strokeWidthPx);
}
@override
Rectangle<int> get componentBounds => this._drawAreaBounds;
@override
bool get isSeriesRenderer => false;
}
/// Rendering information for a slider control element.
class _SliderElement<D> {
Point<int> domainCenterPoint;
Rectangle<int> buttonBounds;
Color fill;
Color stroke;
double strokeWidthPx;
_SliderElement<D> clone() {
return new _SliderElement<D>()
..domainCenterPoint = this.domainCenterPoint
..buttonBounds = this.buttonBounds
..fill = this.fill
..stroke = this.stroke
..strokeWidthPx = this.strokeWidthPx;
}
void updateAnimationPercent(
_SliderElement previous, _SliderElement target, double animationPercent) {
final _SliderElement localPrevious = previous;
final _SliderElement localTarget = target;
final previousPoint = localPrevious.domainCenterPoint;
final targetPoint = localTarget.domainCenterPoint;
final x = ((targetPoint.x - previousPoint.x) * animationPercent) +
previousPoint.x;
final y = ((targetPoint.y - previousPoint.y) * animationPercent) +
previousPoint.y;
domainCenterPoint = new Point<int>(x.round(), y.round());
final previousBounds = localPrevious.buttonBounds;
final targetBounds = localTarget.buttonBounds;
final top = ((targetBounds.top - previousBounds.top) * animationPercent) +
previousBounds.top;
final right =
((targetBounds.right - previousBounds.right) * animationPercent) +
previousBounds.right;
final bottom =
((targetBounds.bottom - previousBounds.bottom) * animationPercent) +
previousBounds.bottom;
final left =
((targetBounds.left - previousBounds.left) * animationPercent) +
previousBounds.left;
buttonBounds = new Rectangle<int>(left.round(), top.round(),
(right - left).round(), (bottom - top).round());
fill = getAnimatedColor(previous.fill, target.fill, animationPercent);
stroke = getAnimatedColor(previous.stroke, target.stroke, animationPercent);
strokeWidthPx =
(((target.strokeWidthPx - previous.strokeWidthPx) * animationPercent) +
previous.strokeWidthPx);
}
}
/// Animates the slider control element of the behavior between different
/// states.
class _AnimatedSlider<D> {
_SliderElement<D> _previousSlider;
_SliderElement<D> _targetSlider;
_SliderElement<D> _currentSlider;
// Flag indicating whether this point is being animated out of the chart.
bool animatingOut = false;
_AnimatedSlider();
/// Animates a point that was removed from the series out of the view.
///
/// This should be called in place of "setNewTarget" for points that represent
/// data that has been removed from the series.
///
/// Animates the width of the slider down to 0.
void animateOut() {
final newTarget = _currentSlider.clone();
// Animate the button bounds inwards horizontally towards a 0 width box.
final targetBounds = newTarget.buttonBounds;
final top = targetBounds.top;
final right = targetBounds.left + targetBounds.width / 2;
final bottom = targetBounds.bottom;
final left = right;
newTarget.buttonBounds = new Rectangle<int>(left.round(), top.round(),
(right - left).round(), (bottom - top).round());
// Animate the stroke width to 0 so that we don't get a lingering line after
// animation is done.
newTarget.strokeWidthPx = 0.0;
setNewTarget(newTarget);
animatingOut = true;
}
void setNewTarget(_SliderElement<D> newTarget) {
animatingOut = false;
_currentSlider ??= newTarget.clone();
_previousSlider = _currentSlider.clone();
_targetSlider = newTarget;
}
_SliderElement<D> getCurrentSlider(double animationPercent) {
if (animationPercent == 1.0 || _previousSlider == null) {
_currentSlider = _targetSlider;
_previousSlider = _targetSlider;
return _currentSlider;
}
_currentSlider.updateAnimationPercent(
_previousSlider, _targetSlider, animationPercent);
return _currentSlider;
}
}
/// Event handler for slider events.
class SliderEventListener<D> {
/// Called when the position of the slider has changed during a drag event.
final SliderListenerCallback<D> onChange;
SliderEventListener({this.onChange});
}
/// Callback function for [Slider] drag events.
///
/// [point] is the current position of the slider line. [point.x] is the domain
/// position, and [point.y] is the position of the center of the line on the
/// measure axis.
///
/// [domain] is the domain value at the slider position.
///
/// [dragState] indicates the current state of a drag event.
typedef SliderListenerCallback<D>(Point<int> point, D domain, String roleId,
SliderListenerDragState dragState);
/// Describes the current state of a slider change as a result of a drag event.
///
/// [initial] indicates that the slider was set to an initial position when new
/// data was drawn on a chart. This will be fired if an initialDomainValue is
/// passed to [Slider]. It will also be fired if the position of the slider
/// changes as a result of new data being drawn on the chart.
///
/// [drag] indicates that the slider is being moved as a result of drag events.
/// When this is passed, the drag event is still active. Once the drag event is
/// completed, an [end] event will be fired.
///
/// [end] indicates that a drag event has been completed. This usually occurs
/// after one or more [drag] events. An [end] event will also be fired if
/// [Slider.moveSliderToDomain] is called, but there will be no preceding [drag]
/// events in this case.
enum SliderListenerDragState { initial, drag, end }
/// Helper class that exposes fewer private internal properties for unit tests.
@visibleForTesting
class SliderTester<D> {
final Slider<D> behavior;
SliderTester(this.behavior);
Point<int> get domainCenterPoint => behavior._domainCenterPoint;
D get domainValue => behavior._domainValue;
Rectangle<int> get handleBounds => behavior._handleBounds;
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
behavior._view.layout(componentBounds, drawAreaBounds);
}
_SliderLayoutView get view => behavior._view;
}

View File

@@ -0,0 +1,75 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../base_chart.dart' show BaseChart;
import '../selection_model/selection_model.dart'
show SelectionModel, SelectionModelType;
import 'chart_behavior.dart' show ChartBehavior;
/// Chart behavior that centers the viewport on the selected domain.
///
/// It is used in combination with SelectNearest to update the selection model
/// and notify this behavior to update the viewport on selection change.
///
/// This behavior can only be used on [CartesianChart].
class SlidingViewport<D> implements ChartBehavior<D> {
final SelectionModelType selectionModelType;
CartesianChart<D> _chart;
SlidingViewport([this.selectionModelType = SelectionModelType.info]);
void _selectionChanged(SelectionModel selectionModel) {
if (selectionModel.hasAnySelection == false) {
return;
}
// Calculate current viewport center and determine the translate pixels
// needed based on the selected domain value's location and existing amount
// of translate pixels.
final domainAxis = _chart.domainAxis;
final selectedDatum = selectionModel.selectedDatum.first;
final domainLocation = domainAxis
.getLocation(selectedDatum.series.domainFn(selectedDatum.index));
final viewportCenter =
domainAxis.range.start + (domainAxis.range.width / 2);
final translatePx =
domainAxis.viewportTranslatePx + (viewportCenter - domainLocation);
domainAxis.setViewportSettings(
domainAxis.viewportScalingFactor, translatePx);
_chart.redraw();
}
@override
void attachTo(BaseChart<D> chart) {
assert(chart is CartesianChart);
_chart = chart as CartesianChart<D>;
chart
.getSelectionModel(selectionModelType)
.addSelectionChangedListener(_selectionChanged);
}
@override
void removeFrom(BaseChart chart) {
chart
.getSelectionModel(selectionModelType)
.removeSelectionChangedListener(_selectionChanged);
}
@override
String get role => 'slidingViewport-${selectionModelType.toString()}';
}

View File

@@ -0,0 +1,264 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point;
import 'package:meta/meta.dart' show protected;
import '../../../../common/gesture_listener.dart' show GestureListener;
import '../../../cartesian/axis/axis.dart' show Axis;
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../../base_chart.dart' show BaseChart, LifecycleListener;
import '../chart_behavior.dart' show ChartBehavior;
/// Adds initial hint behavior for [CartesianChart].
///
/// This behavior animates to the final viewport from an initial translate and
/// or scale factor.
abstract class InitialHintBehavior<D> implements ChartBehavior<D> {
/// Listens for drag gestures.
GestureListener _listener;
/// Chart lifecycle listener to setup hint animation.
LifecycleListener<D> _lifecycleListener;
@override
String get role => 'InitialHint';
/// The chart to which the behavior is attached.
CartesianChart<D> _chart;
@protected
CartesianChart<D> get chart => _chart;
Duration _hintDuration = new Duration(milliseconds: 3000);
/// The amount of time to animate to the desired viewport.
///
/// If no duration is passed in, the default of 3000 ms is used.
@protected
Duration get hintDuration => _hintDuration;
set hintDuration(Duration duration) {
_hintDuration = duration;
}
double _maxHintTranslate = 0.0;
// TODO: Translation animation only works for ordinal axis.
/// The maximum amount ordinal values to shift the viewport for the the hint
/// animation.
///
/// Positive numbers shift the viewport to the right and negative to the left.
/// The default is no translation.
@protected
double get maxHintTranslate => _maxHintTranslate;
set maxHintTranslate(double maxHintTranslate) {
_maxHintTranslate = maxHintTranslate;
}
double _maxHintScaleFactor;
/// The amount the domain axis will be scaled for the start of the hint.
///
/// A value of 1.0 means the viewport is completely zoomed out (all domains
/// are in the viewport). If a value is provided, it cannot be less than 1.0.
///
/// By default maxHintScaleFactor is not set.
@protected
double get maxHintScaleFactor => _maxHintScaleFactor;
set maxHintScaleFactor(double maxHintScaleFactor) {
assert(maxHintScaleFactor != null && maxHintScaleFactor >= 1.0);
_maxHintScaleFactor = maxHintScaleFactor;
}
/// Flag to indicate that hint animation controller has already been set up.
///
/// This is to ensure that the hint is only set up on the first draw.
bool _hintSetupCompleted = false;
/// Flag to indicate that the first call to axis configured is completed.
///
/// This is to ensure that the initial and target viewport translate and scale
/// factor is only calculated on the first axis configuration.
bool _firstAxisConfigured = false;
double _initialViewportTranslatePx;
double _initialViewportScalingFactor;
double _targetViewportTranslatePx;
double _targetViewportScalingFactor;
InitialHintBehavior() {
_listener = new GestureListener(onTapTest: onTapTest);
_lifecycleListener = new LifecycleListener<D>(
onAxisConfigured: _onAxisConfigured,
onAnimationComplete: _onAnimationComplete);
}
@override
attachTo(BaseChart<D> chart) {
if (!(chart is CartesianChart)) {
throw new ArgumentError(
'InitialHintBehavior can only be attached to a CartesianChart');
}
_chart = chart;
_chart.addGestureListener(_listener);
_chart.addLifecycleListener(_lifecycleListener);
}
@override
removeFrom(BaseChart<D> chart) {
if (!(chart is CartesianChart)) {
throw new ArgumentError(
'InitialHintBehavior can only be removed from a CartesianChart');
}
stopHintAnimation();
_chart = chart;
_chart.removeGestureListener(_listener);
_chart.removeLifecycleListener(_lifecycleListener);
_chart = null;
}
@protected
bool onTapTest(Point<double> localPosition) {
if (_chart == null) {
return false;
}
// If the user taps the chart, stop the hint animation immediately.
stopHintAnimation();
return _chart.withinDrawArea(localPosition);
}
/// Calculate the animation's initial and target viewport and scale factor
/// and shift the viewport to the start.
void _onAxisConfigured() {
if (_firstAxisConfigured == false) {
_firstAxisConfigured = true;
final domainAxis = chart.domainAxis;
// TODO: Translation animation only works for axis with a
// rangeband type that returns a non zero step size. If two rows have
// the same domain value, step size could also equal 0.
assert(domainAxis.stepSize != 0.0);
// Save the target viewport and scale factor from axis, because the
// viewport can be set by the user using AxisSpec.
_targetViewportTranslatePx = domainAxis.viewportTranslatePx;
_targetViewportScalingFactor = domainAxis.viewportScalingFactor;
// Calculate the amount to translate from the target viewport.
final translateAmount = domainAxis.stepSize * maxHintTranslate;
_initialViewportTranslatePx =
_targetViewportTranslatePx - translateAmount;
_initialViewportScalingFactor =
maxHintScaleFactor ?? _targetViewportScalingFactor;
domainAxis.setViewportSettings(
_initialViewportScalingFactor, _initialViewportTranslatePx);
chart.redraw(skipAnimation: true, skipLayout: false);
}
}
/// Start the hint animation, only start the animation on the very first draw.
void _onAnimationComplete() {
if (_hintSetupCompleted == false) {
_hintSetupCompleted = true;
startHintAnimation();
}
}
/// Setup and start the hint animation.
///
/// Animation controller to be handled by the native platform.
@protected
void startHintAnimation() {
// When panning starts, measure tick provider should not update ticks.
// This is still needed because axis internally updates the tick location
// after the tick provider generates the ticks. If we do not tell the axis
// not to update the location of the measure axes, the measure axis will
// change during the hint animation and make values jump back and forth.
_chart.getMeasureAxis().lockAxis = true;
_chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis = true;
}
/// Stop hint animation
@protected
void stopHintAnimation() {
// When panning is completed, unlock the measure axis.
_chart.getMeasureAxis().lockAxis = false;
_chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis =
false;
}
/// Animation hint percent, to be returned by the native platform.
@protected
double get hintAnimationPercent;
/// Shift domain viewport on hint animation ticks.
@protected
void onHintTick() {
final percent = hintAnimationPercent;
final scaleFactor = _lerpDouble(
_initialViewportScalingFactor, _targetViewportScalingFactor, percent);
double translatePx = _lerpDouble(
_initialViewportTranslatePx, _targetViewportTranslatePx, percent);
// If there is a scale factor animation, need to scale the translatePx so
// the animation appears to be zooming in on the viewport when there is no
// [maxHintTranslate] provided.
//
// If there is a translate hint, the animation will still first zoom in
// and then translate the [maxHintTranslate] amount.
if (_initialViewportScalingFactor != _targetViewportScalingFactor) {
translatePx = translatePx * percent;
}
final domainAxis = chart.domainAxis;
domainAxis.setViewportSettings(scaleFactor, translatePx,
drawAreaWidth: chart.drawAreaBounds.width);
if (percent >= 1.0) {
stopHintAnimation();
chart.redraw();
} else {
chart.redraw(skipAnimation: true, skipLayout: true);
}
}
/// Linear interpolation for doubles.
double _lerpDouble(double a, double b, double t) {
if (a == null && b == null) return null;
a ??= 0.0;
b ??= 0.0;
return a + (b - a) * t;
}
}

View File

@@ -0,0 +1,119 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show min, max, Point;
import 'package:meta/meta.dart' show protected;
import 'pan_behavior.dart';
import 'panning_tick_provider.dart' show PanningTickProviderMode;
/// Adds domain axis panning and zooming support to the chart.
///
/// Zooming is supported for the web by mouse wheel events. Scrolling up zooms
/// the chart in, and scrolling down zooms the chart out. The chart can never be
/// zoomed out past the domain axis range.
///
/// Zooming is supported by pinch gestures for mobile devices.
///
/// Panning is supported by clicking and dragging the mouse for web, or tapping
/// and dragging on the chart for mobile devices.
class PanAndZoomBehavior<D> extends PanBehavior<D> {
@override
String get role => 'PanAndZoom';
/// Flag which is enabled to indicate that the user is "zooming" the chart.
bool _isZooming = false;
@protected
bool get isZooming => _isZooming;
/// Current zoom scaling factor for the behavior.
double _scalingFactor = 1.0;
/// Minimum scalingFactor to prevent zooming out beyond the data range.
final _minScalingFactor = 1.0;
/// Maximum scalingFactor to prevent zooming in so far that no data is
/// visible.
///
/// TODO: Dynamic max based on data range?
final _maxScalingFactor = 5.0;
@override
bool onDragStart(Point<double> localPosition) {
if (chart == null) {
return false;
}
super.onDragStart(localPosition);
// Save the current scaling factor to make zoom events relative.
_scalingFactor = chart.domainAxis?.viewportScalingFactor;
_isZooming = true;
return true;
}
@override
bool onDragUpdate(Point<double> localPosition, double scale) {
// Swipe gestures should be handled by the [PanBehavior].
if (scale == 1.0) {
_isZooming = false;
return super.onDragUpdate(localPosition, scale);
}
// No further events in this chain should be handled by [PanBehavior].
cancelPanning();
if (!_isZooming || lastPosition == null || chart == null) {
return false;
}
// Update the domain axis's viewport scale factor to zoom the chart.
final domainAxis = chart.domainAxis;
if (domainAxis == null) {
return false;
}
// This is set during onDragUpdate and NOT onDragStart because we don't yet
// know during onDragStart whether pan/zoom behavior is panning or zooming.
// During zoom in / zoom out, domain tick provider set to return existing
// cached ticks.
domainAxisTickProvider.mode = PanningTickProviderMode.useCachedTicks;
// Clamp the scale to prevent zooming out beyond the range of the data, or
// zooming in so far that we show nothing useful.
final newScalingFactor =
min(max(_scalingFactor * scale, _minScalingFactor), _maxScalingFactor);
domainAxis.setViewportSettings(
newScalingFactor, domainAxis.viewportTranslatePx,
drawAreaWidth: chart.drawAreaBounds.width);
chart.redraw(skipAnimation: true, skipLayout: true);
return true;
}
@override
bool onDragEnd(
Point<double> localPosition, double scale, double pixelsPerSec) {
_isZooming = false;
return super.onDragEnd(localPosition, scale, pixelsPerSec);
}
}

View File

@@ -0,0 +1,221 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point;
import 'package:meta/meta.dart' show protected;
import '../../../../common/gesture_listener.dart' show GestureListener;
import '../../../cartesian/axis/axis.dart' show Axis;
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
import '../../base_chart.dart' show BaseChart;
import '../chart_behavior.dart' show ChartBehavior;
import 'panning_tick_provider.dart';
/// Adds domain axis panning support to a chart.
///
/// Panning is supported by clicking and dragging the mouse for web, or tapping
/// and dragging on the chart for mobile devices.
class PanBehavior<D> implements ChartBehavior<D> {
/// Listens for drag gestures.
GestureListener _listener;
/// Wrapped domain tick provider for pan and zoom behavior.
PanningTickProvider _domainAxisTickProvider;
@protected
PanningTickProvider get domainAxisTickProvider => _domainAxisTickProvider;
@override
String get role => 'Pan';
/// The chart to which the behavior is attached.
CartesianChart<D> _chart;
@protected
CartesianChart<D> get chart => _chart;
/// Flag which is enabled to indicate that the user is "panning" the chart.
bool _isPanning = false;
@protected
bool get isPanning => _isPanning;
/// Last position of the mouse/tap that was used to adjust the scale translate
/// factor.
Point<double> _lastPosition;
@protected
Point<double> get lastPosition => _lastPosition;
/// Optional callback that is invoked at the end of panning ([onPanEnd]).
PanningCompletedCallback _panningCompletedCallback;
set panningCompletedCallback(PanningCompletedCallback callback) {
_panningCompletedCallback = callback;
}
PanBehavior() {
_listener = new GestureListener(
onTapTest: onTapTest,
onDragStart: onDragStart,
onDragUpdate: onDragUpdate,
onDragEnd: onDragEnd);
}
/// Injects the behavior into a chart.
@override
attachTo(BaseChart<D> chart) {
if (!(chart is CartesianChart)) {
throw new ArgumentError(
'PanBehavior can only be attached to a CartesianChart');
}
_chart = chart;
_chart.addGestureListener(_listener);
// Disable the autoViewport feature to enable panning.
_chart.domainAxis?.autoViewport = false;
// Wrap domain axis tick provider with the panning behavior one.
_domainAxisTickProvider =
new PanningTickProvider<D>(_chart.domainAxis.tickProvider);
_chart.domainAxis.tickProvider = _domainAxisTickProvider;
}
/// Removes the behavior from a chart.
@override
removeFrom(BaseChart<D> chart) {
if (!(chart is CartesianChart)) {
throw new ArgumentError(
'PanBehavior can only be attached to a CartesianChart');
}
_chart = chart;
_chart.removeGestureListener(_listener);
// Restore the default autoViewport state.
_chart.domainAxis?.autoViewport = true;
// Restore the original tick providers
_chart.domainAxis.tickProvider = _domainAxisTickProvider.tickProvider;
_chart = null;
}
@protected
bool onTapTest(Point<double> localPosition) {
if (_chart == null) {
return false;
}
return _chart.withinDrawArea(localPosition);
}
@protected
bool onDragStart(Point<double> localPosition) {
if (_chart == null) {
return false;
}
onPanStart();
_lastPosition = localPosition;
_isPanning = true;
return true;
}
@protected
bool onDragUpdate(Point<double> localPosition, double scale) {
if (!_isPanning || _lastPosition == null || _chart == null) {
return false;
}
// Pinch gestures should be handled by the [PanAndZoomBehavior].
if (scale != 1.0) {
_isPanning = false;
return false;
}
// Update the domain axis's viewport translate to pan the chart.
final domainAxis = _chart.domainAxis;
if (domainAxis == null) {
return false;
}
// This is set during onDragUpdate and NOT onDragStart because we don't yet
// know during onDragStart whether pan/zoom behavior is panning or zooming.
// During panning, domain tick provider set to generate ticks with locked
// steps.
_domainAxisTickProvider.mode = PanningTickProviderMode.stepSizeLocked;
double domainScalingFactor = domainAxis.viewportScalingFactor;
double domainChange =
domainAxis.viewportTranslatePx + localPosition.x - _lastPosition.x;
domainAxis.setViewportSettings(domainScalingFactor, domainChange,
drawAreaWidth: chart.drawAreaBounds.width);
_lastPosition = localPosition;
_chart.redraw(skipAnimation: true, skipLayout: true);
return true;
}
@protected
bool onDragEnd(
Point<double> localPosition, double scale, double pixelsPerSec) {
onPanEnd();
return true;
}
@protected
void onPanStart() {
// When panning starts, measure tick provider should not update ticks.
// This is still needed because axis internally updates the tick location
// after the tick provider generates the ticks. If we do not tell the axis
// not to update the location of the measure axes, we get a jittery effect
// as the measure axes location changes ever so slightly during pan/zoom.
_chart.getMeasureAxis().lockAxis = true;
_chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis = true;
}
@protected
void onPanEnd() {
cancelPanning();
// When panning stops, allow tick provider to update ticks, and then
// request redraw.
_domainAxisTickProvider.mode = PanningTickProviderMode.passThrough;
_chart.getMeasureAxis().lockAxis = false;
_chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis =
false;
_chart.redraw();
if (_panningCompletedCallback != null) {
_panningCompletedCallback();
}
}
/// Cancels the handling of any current panning event.
void cancelPanning() {
_isPanning = false;
}
}
/// Callback for when panning is completed.
typedef void PanningCompletedCallback();

View File

@@ -0,0 +1,90 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show required;
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
import '../../../cartesian/axis/axis.dart' show AxisOrientation;
import '../../../cartesian/axis/draw_strategy/tick_draw_strategy.dart'
show TickDrawStrategy;
import '../../../cartesian/axis/scale.dart' show MutableScale;
import '../../../cartesian/axis/tick.dart' show Tick;
import '../../../cartesian/axis/tick_formatter.dart' show TickFormatter;
import '../../../cartesian/axis/tick_provider.dart' show TickProvider, TickHint;
import '../../../common/chart_context.dart' show ChartContext;
enum PanningTickProviderMode {
/// Return cached ticks.
useCachedTicks,
/// Request ticks with [TickHint] calculated from cached ticks.
stepSizeLocked,
/// Request ticks directly from tick provider.
passThrough,
}
/// Wraps an existing tick provider to be able to return cached ticks during
/// zoom in/out, return ticks calculated with locked step size during panning,
/// or just pass through to the existing tick provider.
class PanningTickProvider<D> implements TickProvider<D> {
final TickProvider<D> tickProvider;
PanningTickProviderMode _mode = PanningTickProviderMode.passThrough;
List<Tick<D>> _ticks;
PanningTickProvider(this.tickProvider);
set mode(PanningTickProviderMode mode) {
_mode = mode;
}
List<Tick<D>> getTicks({
@required ChartContext context,
@required GraphicsFactory graphicsFactory,
@required MutableScale<D> scale,
@required TickFormatter<D> formatter,
@required Map<D, String> formatterValueCache,
@required TickDrawStrategy tickDrawStrategy,
@required AxisOrientation orientation,
bool viewportExtensionEnabled = false,
TickHint<D> tickHint,
}) {
if (_mode == PanningTickProviderMode.stepSizeLocked) {
tickHint = new TickHint(
_ticks.first.value,
_ticks.last.value,
tickCount: _ticks.length,
);
}
if (_mode != PanningTickProviderMode.useCachedTicks) {
_ticks = tickProvider.getTicks(
context: context,
graphicsFactory: graphicsFactory,
scale: scale,
formatter: formatter,
formatterValueCache: formatterValueCache,
tickDrawStrategy: tickDrawStrategy,
orientation: orientation,
viewportExtensionEnabled: viewportExtensionEnabled,
tickHint: tickHint,
);
}
return _ticks;
}
}

View File

@@ -0,0 +1,125 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle, min, max, Point;
import '../../common/color.dart' show Color;
import 'chart_canvas.dart' show FillPatternType;
/// A rectangle to be painted by [ChartCanvas].
class CanvasRect {
final Rectangle<int> bounds;
final List<int> dashPattern;
final Color fill;
final FillPatternType pattern;
final Color stroke;
final double strokeWidthPx;
CanvasRect(this.bounds,
{this.dashPattern,
this.fill,
this.pattern,
this.stroke,
this.strokeWidthPx});
}
/// A stack of [CanvasRect] to be painted by [ChartCanvas].
class CanvasBarStack {
final List<CanvasRect> segments;
final int radius;
final int stackedBarPadding;
final bool roundTopLeft;
final bool roundTopRight;
final bool roundBottomLeft;
final bool roundBottomRight;
final Rectangle<int> fullStackRect;
factory CanvasBarStack(List<CanvasRect> segments,
{int radius,
int stackedBarPadding,
bool roundTopLeft,
bool roundTopRight,
bool roundBottomLeft,
bool roundBottomRight}) {
final firstBarBounds = segments.first.bounds;
// Find the rectangle that would represent the full stack of bars.
var left = firstBarBounds.left;
var top = firstBarBounds.top;
var right = firstBarBounds.right;
var bottom = firstBarBounds.bottom;
for (var barIndex = 1; barIndex < segments.length; barIndex++) {
final bounds = segments[barIndex].bounds;
left = min(left, bounds.left);
top = min(top, bounds.top);
right = max(right, bounds.right);
bottom = max(bottom, bounds.bottom);
}
final width = right - left;
final height = bottom - top;
final fullStackRect = new Rectangle(left, top, width, height);
return new CanvasBarStack._internal(
segments,
radius: radius,
stackedBarPadding: stackedBarPadding,
roundTopLeft: roundTopLeft,
roundTopRight: roundTopRight,
roundBottomLeft: roundBottomLeft,
roundBottomRight: roundBottomRight,
fullStackRect: fullStackRect,
);
}
CanvasBarStack._internal(
this.segments, {
this.radius,
this.stackedBarPadding = 1,
this.roundTopLeft = false,
this.roundTopRight = false,
this.roundBottomLeft = false,
this.roundBottomRight = false,
this.fullStackRect,
});
}
/// A list of [CanvasPieSlice]s to be painted by [ChartCanvas].
class CanvasPie {
final List<CanvasPieSlice> slices;
Point center;
double radius;
double innerRadius;
/// Color of separator lines between arcs.
final Color stroke;
/// Stroke width of separator lines between arcs.
double strokeWidthPx;
CanvasPie(this.slices, this.center, this.radius, this.innerRadius,
{this.stroke, this.strokeWidthPx = 0.0});
}
/// A circle sector to be painted by [ChartCanvas].
class CanvasPieSlice {
double startAngle;
double endAngle;
Color fill;
CanvasPieSlice(this.startAngle, this.endAngle, {this.fill});
}

View File

@@ -0,0 +1,163 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point, Rectangle;
import '../../common/color.dart' show Color;
import '../../common/graphics_factory.dart' show GraphicsFactory;
import '../../common/text_element.dart' show TextElement;
import 'canvas_shapes.dart' show CanvasBarStack, CanvasPie;
abstract class ChartCanvas {
/// Get [GraphicsFactory] for creating native graphics elements.
GraphicsFactory get graphicsFactory;
/// Set the name of the view doing the rendering for debugging purposes,
/// or null when we believe rendering is complete.
set drawingView(String viewName);
/// Renders a sector of a circle, with an optional hole in the center.
///
/// [center] The x, y coordinates of the circle's center.
/// [radius] The radius of the circle.
/// [innerRadius] Optional radius of a hole in the center of the circle that
/// should not be filled in as part of the sector.
/// [startAngle] The angle at which the arc starts, measured clockwise from
/// the positive x axis and expressed in radians
/// [endAngle] The angle at which the arc ends, measured clockwise from the
/// positive x axis and expressed in radians.
/// [fill] Fill color for the sector.
/// [stroke] Stroke color of the arc and radius lines.
/// [strokeWidthPx] Stroke width of the arc and radius lines.
void drawCircleSector(Point center, double radius, double innerRadius,
double startAngle, double endAngle,
{Color fill, Color stroke, double strokeWidthPx});
/// Renders a simple line.
///
/// [dashPattern] controls the pattern of dashes and gaps in a line. It is a
/// list of lengths of alternating dashes and gaps. The rendering is similar
/// to stroke-dasharray in SVG path elements. An odd number of values in the
/// pattern will be repeated to derive an even number of values. "1,2,3" is
/// equivalent to "1,2,3,1,2,3."
void drawLine(
{List<Point> points,
Rectangle<num> clipBounds,
Color fill,
Color stroke,
bool roundEndCaps,
double strokeWidthPx,
List<int> dashPattern});
/// Renders a pie, with an optional hole in the center.
void drawPie(CanvasPie canvasPie);
/// Renders a simple point.
///
/// [point] The x, y coordinates of the point.
///
/// [radius] The radius of the point.
///
/// [fill] Fill color for the point.
///
/// [stroke] and [strokeWidthPx] configure the color and thickness of the
/// outer edge of the point. Both must be provided together for a line to
/// appear.
void drawPoint(
{Point point,
double radius,
Color fill,
Color stroke,
double strokeWidthPx});
/// Renders a polygon shape described by a set of points.
///
/// [points] describes the vertices of the polygon. The last point will always
/// be connected to the first point to close the shape.
///
/// [fill] configures the color inside the polygon. The shape will be
/// transparent if this is not provided.
///
/// [stroke] and [strokeWidthPx] configure the color and thickness of the
/// edges of the polygon. Both must be provided together for a line to appear.
void drawPolygon(
{List<Point> points,
Rectangle<num> clipBounds,
Color fill,
Color stroke,
double strokeWidthPx});
/// Renders a simple rectangle.
///
/// [drawAreaBounds] if specified and if the bounds of the rectangle exceed
/// the draw area bounds on the top, the first x pixels (decided by the native
/// platform) exceeding the draw area will apply a gradient to transparent
/// with anything exceeding the x pixels to be transparent.
void drawRect(Rectangle<num> bounds,
{Color fill,
Color stroke,
double strokeWidthPx,
Rectangle<num> drawAreaBounds});
/// Renders a rounded rectangle.
void drawRRect(Rectangle<num> bounds,
{Color fill,
Color stroke,
num radius,
bool roundTopLeft,
bool roundTopRight,
bool roundBottomLeft,
bool roundBottomRight});
/// Renders a stack of bars, rounding the last bar in the stack.
///
/// The first bar of the stack is expected to be the "base" bar. This would
/// be the bottom most bar for a vertically rendered bar.
///
/// [drawAreaBounds] if specified and if the bounds of the rectangle exceed
/// the draw area bounds on the top, the first x pixels (decided by the native
/// platform) exceeding the draw area will apply a gradient to transparent
/// with anything exceeding the x pixels to be transparent.
void drawBarStack(CanvasBarStack canvasBarStack,
{Rectangle<num> drawAreaBounds});
void drawText(TextElement textElement, int offsetX, int offsetY,
{double rotation = 0.0});
/// Request the canvas to clip to [clipBounds].
///
/// Applies to all operations until [restClipBounds] is called.
void setClipBounds(Rectangle<int> clipBounds);
/// Restore
void resetClipBounds();
}
Color getAnimatedColor(Color previous, Color target, double animationPercent) {
var r = (((target.r - previous.r) * animationPercent) + previous.r).round();
var g = (((target.g - previous.g) * animationPercent) + previous.g).round();
var b = (((target.b - previous.b) * animationPercent) + previous.b).round();
var a = (((target.a - previous.a) * animationPercent) + previous.a).round();
return new Color(a: a, r: r, g: g, b: b);
}
/// Defines the pattern for a color fill.
///
/// * [forwardHatch] defines a pattern of white lines angled up and to the right
/// on top of a bar filled with the fill color.
/// * [solid] defines a simple bar filled with the fill color. This is the
/// default pattern for bars.
enum FillPatternType { forwardHatch, solid }

View File

@@ -0,0 +1,61 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../common/date_time_factory.dart';
import '../../common/rtl_spec.dart' show RTLSpec;
import '../common/behavior/a11y/a11y_node.dart' show A11yNode;
abstract class ChartContext {
/// Flag indicating whether or not the chart's container was configured in
/// right to left mode.
///
/// This should be set when the chart is created (or if its container ever
/// gets configured to the other direction setting).
///
/// Any chart component that needs to know whether the chart axes should be
/// rendered right to left should read [isRtl].
bool get chartContainerIsRtl;
/// Configures the behavior of the chart when [chartContainerIsRtl] is true.
RTLSpec get rtlSpec;
/// Gets whether or not the chart axes should be rendered in right to left
/// mode.
///
/// This will only be true if the container for the chart component was
/// configured with the rtl direction setting ([chartContainerIsRtl] == true), and the chart's
/// [RTLSpec] is set to reverse the axis direction in rtl mode.
bool get isRtl;
/// Whether or not the chart will respond to tap events.
///
/// This will generally be true if there is a behavior attached to the chart
/// that does something with tap events, such as "click to select data."
bool get isTappable;
double get pixelsPerDp;
DateTimeFactory get dateTimeFactory;
void requestRedraw();
void requestAnimation(Duration transition);
void requestPaint();
void enableA11yExploreMode(List<A11yNode> nodes, {String announcement});
void disableA11yExploreMode({String announcement});
}

View File

@@ -0,0 +1,222 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point;
import '../../common/color.dart' show Color;
import '../../common/symbol_renderer.dart' show SymbolRenderer;
import 'processed_series.dart' show ImmutableSeries;
typedef String DomainFormatter<D>(D domain);
typedef String MeasureFormatter(num measure);
/// Represents processed rendering details for a data point from a series.
class DatumDetails<D> {
final dynamic datum;
/// The index of the datum in the series.
final int index;
/// Domain value of [datum].
final D domain;
/// Domain lower bound value of [datum]. This may represent an error bound, or
/// a previous domain value.
final D domainLowerBound;
/// Domain upper bound value of [datum]. This may represent an error bound, or
/// a target domain value.
final D domainUpperBound;
/// Measure value of [datum].
final num measure;
/// Measure lower bound value of [datum]. This may represent an error bound,
/// or a previous value.
final num measureLowerBound;
/// Measure upper bound value of [datum]. This may represent an error bound,
/// or a target measure value.
final num measureUpperBound;
/// Measure offset value of [datum].
final num measureOffset;
/// Original measure value of [datum]. This may differ from [measure] if a
/// behavior attached to a chart automatically adjusts measure values.
final num rawMeasure;
/// Original measure lower bound value of [datum]. This may differ from
/// [measureLowerBound] if a behavior attached to a chart automatically
/// adjusts measure values.
final num rawMeasureLowerBound;
/// Original measure upper bound value of [datum]. This may differ from
/// [measureUpperBound] if a behavior attached to a chart automatically
/// adjusts measure values.
final num rawMeasureUpperBound;
/// The series the [datum] is from.
final ImmutableSeries<D> series;
/// The color of this [datum].
final Color color;
/// Optional fill color of this [datum].
///
/// If this is defined, then [color] will be used as a stroke color.
/// Otherwise, [color] will be used for the fill color.
final Color fillColor;
/// Optional area color of this [datum].
///
/// This color is used for supplemental information on the series, such as
/// confidence intervals or area skirts. If not provided, then some variation
/// of the main [color] will be used (e.g. 10% opacity).
final Color areaColor;
/// Optional dash pattern of this [datum].
final List<int> dashPattern;
/// The chart position of the (domain, measure) for the [datum] from a
/// renderer.
final Point<double> chartPosition;
/// The chart position of the (domainLowerBound, measureLowerBound) for the
/// [datum] from a renderer.
final Point<double> chartPositionLower;
/// The chart position of the (domainUpperBound, measureUpperBound) for the
/// [datum] from a renderer.
final Point<double> chartPositionUpper;
/// Distance of [domain] from a given (x, y) coordinate.
final double domainDistance;
/// Distance of [measure] from a given (x, y) coordinate.
final double measureDistance;
/// Relative Cartesian distance of ([domain], [measure]) from a given (x, y)
/// coordinate.
final double relativeDistance;
/// The radius of this [datum].
final double radiusPx;
/// Renderer used to draw the shape of this datum.
///
/// This is primarily used for point shapes on line and scatter plot charts.
final SymbolRenderer symbolRenderer;
/// The stroke width of this [datum].
final double strokeWidthPx;
/// Optional formatter for [domain].
DomainFormatter<D> domainFormatter;
/// Optional formatter for [measure].
MeasureFormatter measureFormatter;
DatumDetails(
{this.datum,
this.index,
this.domain,
this.domainLowerBound,
this.domainUpperBound,
this.measure,
this.measureLowerBound,
this.measureUpperBound,
this.measureOffset,
this.rawMeasure,
this.rawMeasureLowerBound,
this.rawMeasureUpperBound,
this.series,
this.color,
this.fillColor,
this.areaColor,
this.dashPattern,
this.chartPosition,
this.chartPositionLower,
this.chartPositionUpper,
this.domainDistance,
this.measureDistance,
this.relativeDistance,
this.radiusPx,
this.symbolRenderer,
this.strokeWidthPx});
factory DatumDetails.from(DatumDetails<D> other,
{D datum,
int index,
D domain,
D domainLowerBound,
D domainUpperBound,
num measure,
num measureLowerBound,
num measureUpperBound,
num measureOffset,
num rawMeasure,
num rawMeasureLowerBound,
num rawMeasureUpperBound,
ImmutableSeries<D> series,
Color color,
Color fillColor,
Color areaColor,
List<int> dashPattern,
Point<double> chartPosition,
Point<double> chartPositionLower,
Point<double> chartPositionUpper,
double domainDistance,
double measureDistance,
double radiusPx,
SymbolRenderer symbolRenderer,
double strokeWidthPx}) {
return new DatumDetails<D>(
datum: datum ?? other.datum,
index: index ?? other.index,
domain: domain ?? other.domain,
domainLowerBound: domainLowerBound ?? other.domainLowerBound,
domainUpperBound: domainUpperBound ?? other.domainUpperBound,
measure: measure ?? other.measure,
measureLowerBound: measureLowerBound ?? other.measureLowerBound,
measureUpperBound: measureUpperBound ?? other.measureUpperBound,
measureOffset: measureOffset ?? other.measureOffset,
rawMeasure: rawMeasure ?? other.rawMeasure,
rawMeasureLowerBound:
rawMeasureLowerBound ?? other.rawMeasureLowerBound,
rawMeasureUpperBound:
rawMeasureUpperBound ?? other.rawMeasureUpperBound,
series: series ?? other.series,
color: color ?? other.color,
fillColor: fillColor ?? other.fillColor,
areaColor: areaColor ?? other.areaColor,
dashPattern: dashPattern ?? other.dashPattern,
chartPosition: chartPosition ?? other.chartPosition,
chartPositionLower: chartPositionLower ?? other.chartPositionLower,
chartPositionUpper: chartPositionUpper ?? other.chartPositionUpper,
domainDistance: domainDistance ?? other.domainDistance,
measureDistance: measureDistance ?? other.measureDistance,
radiusPx: radiusPx ?? other.radiusPx,
symbolRenderer: symbolRenderer ?? other.symbolRenderer,
strokeWidthPx: radiusPx ?? other.strokeWidthPx);
}
String get formattedDomain =>
(domainFormatter != null) ? domainFormatter(domain) : domain.toString();
String get formattedMeasure => (measureFormatter != null)
? measureFormatter(measure)
: measure.toString();
}

View File

@@ -0,0 +1,232 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import '../../common/color.dart' show Color;
import '../../data/series.dart'
show AccessorFn, Series, SeriesAttributes, AttributeKey;
import '../cartesian/axis/axis.dart' show Axis;
import '../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
import '../common/chart_canvas.dart' show FillPatternType;
class MutableSeries<D> extends ImmutableSeries<D> {
final String id;
String displayName;
String seriesCategory;
bool overlaySeries;
int seriesIndex;
/// Sum of the measure values for the series.
num seriesMeasureTotal;
List data;
AccessorFn<String> keyFn;
AccessorFn<D> domainFn;
AccessorFn<D> domainLowerBoundFn;
AccessorFn<D> domainUpperBoundFn;
AccessorFn<num> measureFn;
AccessorFn<num> measureLowerBoundFn;
AccessorFn<num> measureUpperBoundFn;
AccessorFn<num> measureOffsetFn;
AccessorFn<num> rawMeasureFn;
AccessorFn<num> rawMeasureLowerBoundFn;
AccessorFn<num> rawMeasureUpperBoundFn;
AccessorFn<Color> areaColorFn;
AccessorFn<Color> colorFn;
AccessorFn<List<int>> dashPatternFn;
AccessorFn<Color> fillColorFn;
AccessorFn<FillPatternType> fillPatternFn;
AccessorFn<num> radiusPxFn;
AccessorFn<num> strokeWidthPxFn;
AccessorFn<String> labelAccessorFn;
AccessorFn<TextStyleSpec> insideLabelStyleAccessorFn;
AccessorFn<TextStyleSpec> outsideLabelStyleAccessorFn;
final _attrs = new SeriesAttributes();
Axis measureAxis;
Axis domainAxis;
MutableSeries(Series<dynamic, D> series) : this.id = series.id {
displayName = series.displayName ?? series.id;
seriesCategory = series.seriesCategory;
overlaySeries = series.overlaySeries;
data = series.data;
keyFn = series.keyFn;
domainFn = series.domainFn;
domainLowerBoundFn = series.domainLowerBoundFn;
domainUpperBoundFn = series.domainUpperBoundFn;
measureFn = series.measureFn;
measureLowerBoundFn = series.measureLowerBoundFn;
measureUpperBoundFn = series.measureUpperBoundFn;
measureOffsetFn = series.measureOffsetFn;
// Save the original measure functions in case they get replaced later.
rawMeasureFn = series.measureFn;
rawMeasureLowerBoundFn = series.measureLowerBoundFn;
rawMeasureUpperBoundFn = series.measureUpperBoundFn;
// Pre-compute the sum of the measure values to make it available on demand.
seriesMeasureTotal = 0;
for (int i = 0; i < data.length; i++) {
final measure = measureFn(i);
if (measure != null) {
seriesMeasureTotal += measure;
}
}
areaColorFn = series.areaColorFn;
colorFn = series.colorFn;
dashPatternFn = series.dashPatternFn;
fillColorFn = series.fillColorFn;
fillPatternFn = series.fillPatternFn;
labelAccessorFn = series.labelAccessorFn ?? (i) => domainFn(i).toString();
insideLabelStyleAccessorFn = series.insideLabelStyleAccessorFn;
outsideLabelStyleAccessorFn = series.outsideLabelStyleAccessorFn;
radiusPxFn = series.radiusPxFn;
strokeWidthPxFn = series.strokeWidthPxFn;
_attrs.mergeFrom(series.attributes);
}
MutableSeries.clone(MutableSeries<D> other) : this.id = other.id {
displayName = other.displayName;
seriesCategory = other.seriesCategory;
overlaySeries = other.overlaySeries;
seriesIndex = other.seriesIndex;
data = other.data;
keyFn = other.keyFn;
domainFn = other.domainFn;
domainLowerBoundFn = other.domainLowerBoundFn;
domainUpperBoundFn = other.domainUpperBoundFn;
measureFn = other.measureFn;
measureLowerBoundFn = other.measureLowerBoundFn;
measureUpperBoundFn = other.measureUpperBoundFn;
measureOffsetFn = other.measureOffsetFn;
rawMeasureFn = other.rawMeasureFn;
rawMeasureLowerBoundFn = other.rawMeasureLowerBoundFn;
rawMeasureUpperBoundFn = other.rawMeasureUpperBoundFn;
seriesMeasureTotal = other.seriesMeasureTotal;
areaColorFn = other.areaColorFn;
colorFn = other.colorFn;
dashPatternFn = other.dashPatternFn;
fillColorFn = other.fillColorFn;
fillPatternFn = other.fillPatternFn;
labelAccessorFn = other.labelAccessorFn;
insideLabelStyleAccessorFn = other.insideLabelStyleAccessorFn;
outsideLabelStyleAccessorFn = other.outsideLabelStyleAccessorFn;
radiusPxFn = other.radiusPxFn;
strokeWidthPxFn = other.strokeWidthPxFn;
_attrs.mergeFrom(other._attrs);
measureAxis = other.measureAxis;
domainAxis = other.domainAxis;
}
void setAttr<R>(AttributeKey<R> key, R value) {
this._attrs.setAttr(key, value);
}
R getAttr<R>(AttributeKey<R> key) {
return this._attrs.getAttr(key);
}
bool operator ==(Object other) =>
other is MutableSeries && data == other.data && id == other.id;
@override
int get hashCode => data.hashCode * 31 + id.hashCode;
}
abstract class ImmutableSeries<D> {
String get id;
String get displayName;
String get seriesCategory;
bool get overlaySeries;
int get seriesIndex;
/// Sum of the measure values for the series.
num get seriesMeasureTotal;
List get data;
/// [keyFn] defines a globally unique identifier for each datum.
///
/// The key for each datum is used during chart animation to smoothly
/// transition data still in the series to its new state.
///
/// Note: This is currently an optional function that is not fully used by all
/// series renderers yet.
AccessorFn<String> keyFn;
AccessorFn<D> get domainFn;
AccessorFn<D> get domainLowerBoundFn;
AccessorFn<D> get domainUpperBoundFn;
AccessorFn<num> get measureFn;
AccessorFn<num> get measureLowerBoundFn;
AccessorFn<num> get measureUpperBoundFn;
AccessorFn<num> get measureOffsetFn;
AccessorFn<num> get rawMeasureFn;
AccessorFn<num> get rawMeasureLowerBoundFn;
AccessorFn<num> get rawMeasureUpperBoundFn;
AccessorFn<Color> get areaColorFn;
AccessorFn<Color> get colorFn;
AccessorFn<List<int>> get dashPatternFn;
AccessorFn<Color> get fillColorFn;
AccessorFn<FillPatternType> get fillPatternFn;
AccessorFn<String> get labelAccessorFn;
AccessorFn<TextStyleSpec> insideLabelStyleAccessorFn;
AccessorFn<TextStyleSpec> outsideLabelStyleAccessorFn;
AccessorFn<num> get radiusPxFn;
AccessorFn<num> get strokeWidthPxFn;
void setAttr<R>(AttributeKey<R> key, R value);
R getAttr<R>(AttributeKey<R> key);
}

Some files were not shown because too many files have changed in this diff Show More