1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-11 07:18:15 +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,191 @@
// 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 'package:charts_common/common.dart'
show
boundsLineRadiusPxFnKey,
boundsLineRadiusPxKey,
measureAxisIdKey,
pointSymbolRendererFnKey,
pointSymbolRendererIdKey,
rendererIdKey,
AnnotationLabelAnchor,
AnnotationLabelDirection,
AnnotationLabelPosition,
ArcLabelDecorator,
ArcLabelLeaderLineStyleSpec,
ArcLabelPosition,
ArcRenderer,
ArcRendererConfig,
AutoDateTimeTickFormatterSpec,
AutoDateTimeTickProviderSpec,
Axis,
AxisDirection,
AxisSpec,
BarGroupingType,
BarLabelAnchor,
BarLabelDecorator,
BarLabelPosition,
BarLaneRendererConfig,
BarRenderer,
BarRendererConfig,
BarTargetLineRenderer,
BarTargetLineRendererConfig,
BaseCartesianRenderer,
BasicNumericTickFormatterSpec,
BasicNumericTickProviderSpec,
BasicOrdinalTickProviderSpec,
BasicOrdinalTickFormatterSpec,
BehaviorPosition,
BucketingAxisSpec,
BucketingNumericTickProviderSpec,
CartesianChart,
ChartCanvas,
ChartContext,
ChartTitleDirection,
CircleSymbolRenderer,
Color,
ComparisonPointsDecorator,
ConstCornerStrategy,
CornerStrategy,
CylinderSymbolRenderer,
DateTimeAxisSpec,
DateTimeEndPointsTickProviderSpec,
DateTimeExtents,
DateTimeFactory,
DateTimeTickFormatter,
DateTimeTickFormatterSpec,
DateTimeTickProviderSpec,
DayTickProviderSpec,
DomainFormatter,
EndPointsTimeAxisSpec,
ExploreModeTrigger,
FillPatternType,
GestureListener,
GraphicsFactory,
GridlineRendererSpec,
ImmutableSeries,
InsideJustification,
LayoutPosition,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
LegendDefaultMeasure,
LegendTapHandling,
LineAnnotationSegment,
LinePointHighlighterFollowLineType,
LineRenderer,
LineRendererConfig,
LineStyleSpec,
LocalDateTimeFactory,
LockSelection,
MarginSpec,
MaterialPalette,
MaterialStyle,
MaxWidthStrategy,
MeasureFormatter,
NoCornerStrategy,
NoneRenderSpec,
NumericAxis,
NumericAxisSpec,
NumericCartesianChart,
NumericEndPointsTickProviderSpec,
NumericExtents,
NumericTickFormatterSpec,
NumericTickProviderSpec,
OrdinalAxis,
OrdinalAxisSpec,
OrdinalCartesianChart,
OrdinalTickFormatterSpec,
OrdinalTickProviderSpec,
OrdinalViewport,
OutsideJustification,
PanningCompletedCallback,
PercentAxisSpec,
PercentInjectorTotalType,
Performance,
PointRenderer,
PointRendererConfig,
PointRendererDecorator,
PointSymbolRenderer,
RangeAnnotationAxisType,
RangeAnnotationSegment,
RectSymbolRenderer,
RenderSpec,
RTLSpec,
SelectionModel,
SelectionModelListener,
SelectionModelType,
SelectionTrigger,
Series,
SeriesDatum,
SeriesDatumConfig,
SeriesRenderer,
SeriesRendererConfig,
SimpleTickFormatterBase,
SliderListenerCallback,
SliderListenerDragState,
SliderStyle,
SmallTickRendererSpec,
StaticDateTimeTickProviderSpec,
StaticNumericTickProviderSpec,
StaticOrdinalTickProviderSpec,
StyleFactory,
SymbolAnnotationRenderer,
SymbolAnnotationRendererConfig,
TextStyleSpec,
TickFormatter,
TickFormatterSpec,
TickLabelAnchor,
TickLabelJustification,
TickSpec,
TimeFormatterSpec,
TypedAccessorFn,
UTCDateTimeFactory,
ViewMargin,
VocalizationCallback;
export 'src/bar_chart.dart';
export 'src/base_chart.dart' show BaseChart, LayoutConfig;
export 'src/behaviors/a11y/domain_a11y_explore_behavior.dart'
show DomainA11yExploreBehavior;
export 'src/behaviors/chart_behavior.dart' show ChartBehavior;
export 'src/behaviors/domain_highlighter.dart' show DomainHighlighter;
export 'src/behaviors/initial_selection.dart' show InitialSelection;
export 'src/behaviors/calculation/percent_injector.dart' show PercentInjector;
export 'src/behaviors/chart_title/chart_title.dart' show ChartTitle;
export 'src/behaviors/legend/datum_legend.dart' show DatumLegend;
export 'src/behaviors/legend/legend_content_builder.dart'
show LegendContentBuilder, TabularLegendContentBuilder;
export 'src/behaviors/legend/legend_layout.dart'
show LegendLayout, TabularLegendLayout;
export 'src/behaviors/legend/series_legend.dart' show SeriesLegend;
export 'src/behaviors/line_point_highlighter.dart' show LinePointHighlighter;
export 'src/behaviors/range_annotation.dart' show RangeAnnotation;
export 'src/behaviors/select_nearest.dart' show SelectNearest;
export 'src/behaviors/sliding_viewport.dart' show SlidingViewport;
export 'src/behaviors/slider/slider.dart' show Slider;
export 'src/behaviors/zoom/initial_hint_behavior.dart' show InitialHintBehavior;
export 'src/behaviors/zoom/pan_and_zoom_behavior.dart' show PanAndZoomBehavior;
export 'src/behaviors/zoom/pan_behavior.dart' show PanBehavior;
export 'src/combo_chart/combo_chart.dart';
export 'src/line_chart.dart';
export 'src/pie_chart.dart';
export 'src/scatter_plot_chart.dart';
export 'src/selection_model_config.dart' show SelectionModelConfig;
export 'src/symbol_renderer.dart' show CustomSymbolRenderer;
export 'src/time_series_chart.dart';
export 'src/user_managed_state.dart'
show UserManagedState, UserManagedSelectionModel;
export 'src/util/color.dart' show ColorUtil;

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 'dart:collection' show LinkedHashMap;
import 'package:charts_common/common.dart' as common
show
AxisSpec,
BarChart,
BarGroupingType,
BarRendererConfig,
BarRendererDecorator,
NumericAxisSpec,
RTLSpec,
Series,
SeriesRendererConfig;
import 'behaviors/domain_highlighter.dart' show DomainHighlighter;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'package:meta/meta.dart' show immutable;
import 'base_chart.dart' show LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'cartesian_chart.dart' show CartesianChart;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
@immutable
class BarChart extends CartesianChart<String> {
final bool vertical;
final common.BarRendererDecorator barRendererDecorator;
BarChart(
List<common.Series<dynamic, String>> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes,
common.BarGroupingType barGroupingType,
common.BarRendererConfig<String> defaultRenderer,
List<common.SeriesRendererConfig<String>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<String>> selectionModels,
common.RTLSpec rtlSpec,
this.vertical: true,
bool defaultInteractions: true,
LayoutConfig layoutConfig,
UserManagedState<String> userManagedState,
this.barRendererDecorator,
bool flipVerticalAxis,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes,
defaultRenderer: defaultRenderer ??
new common.BarRendererConfig<String>(
groupingType: barGroupingType,
barRendererDecorator: barRendererDecorator),
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
defaultInteractions: defaultInteractions,
layoutConfig: layoutConfig,
userManagedState: userManagedState,
flipVerticalAxis: flipVerticalAxis,
);
@override
common.BarChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.BarChart(
vertical: vertical,
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(),
disjointMeasureAxes: createDisjointMeasureAxes());
}
@override
void addDefaultInteractions(List<ChartBehavior> behaviors) {
super.addDefaultInteractions(behaviors);
behaviors.add(new DomainHighlighter());
}
}

View File

@@ -0,0 +1,279 @@
// 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/common.dart' as common
show
BaseChart,
LayoutConfig,
MarginSpec,
Performance,
RTLSpec,
Series,
SeriesRendererConfig,
SelectionModelType,
SelectionTrigger;
import 'behaviors/select_nearest.dart' show SelectNearest;
import 'package:meta/meta.dart' show immutable, required;
import 'behaviors/chart_behavior.dart'
show ChartBehavior, ChartStateBehavior, GestureType;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'package:flutter_web/material.dart' show StatefulWidget;
import 'base_chart_state.dart' show BaseChartState;
import 'user_managed_state.dart' show UserManagedState;
@immutable
abstract class BaseChart<D> extends StatefulWidget {
/// Series list to draw.
final List<common.Series<dynamic, D>> seriesList;
/// Animation transitions.
final bool animate;
final Duration animationDuration;
/// Used to configure the margin sizes around the drawArea that the axis and
/// other things render into.
final LayoutConfig layoutConfig;
// Default renderer used to draw series data on the chart.
final common.SeriesRendererConfig<D> defaultRenderer;
/// Include the default interactions or not.
final bool defaultInteractions;
final List<ChartBehavior> behaviors;
final List<SelectionModelConfig<D>> selectionModels;
// List of custom series renderers used to draw series data on the chart.
//
// Series assigned a rendererIdKey will be drawn with the matching renderer in
// this list. Series without a rendererIdKey will be drawn by the default
// renderer.
final List<common.SeriesRendererConfig<D>> customSeriesRenderers;
/// The spec to use if RTL is enabled.
final common.RTLSpec rtlSpec;
/// Optional state that overrides internally kept state, such as selection.
final UserManagedState<D> userManagedState;
BaseChart(this.seriesList,
{bool animate,
Duration animationDuration,
this.defaultRenderer,
this.customSeriesRenderers,
this.behaviors,
this.selectionModels,
this.rtlSpec,
this.defaultInteractions = true,
this.layoutConfig,
this.userManagedState})
: this.animate = animate ?? true,
this.animationDuration =
animationDuration ?? const Duration(milliseconds: 300);
@override
BaseChartState<D> createState() => new BaseChartState<D>();
/// Creates and returns a [common.BaseChart].
common.BaseChart<D> createCommonChart(BaseChartState<D> chartState);
/// Updates the [common.BaseChart].
void updateCommonChart(common.BaseChart chart, BaseChart<D> oldWidget,
BaseChartState<D> chartState) {
common.Performance.time('chartsUpdateRenderers');
// Set default renderer if one was provided.
if (defaultRenderer != null &&
defaultRenderer != oldWidget?.defaultRenderer) {
chart.defaultRenderer = defaultRenderer.build();
chartState.markChartDirty();
}
// Add custom series renderers if any were provided.
if (customSeriesRenderers != null) {
// TODO: This logic does not remove old renderers and
// shouldn't require the series configs to remain in the same order.
for (var i = 0; i < customSeriesRenderers.length; i++) {
if (oldWidget == null ||
(oldWidget.customSeriesRenderers != null &&
i > oldWidget.customSeriesRenderers.length) ||
customSeriesRenderers[i] != oldWidget.customSeriesRenderers[i]) {
chart.addSeriesRenderer(customSeriesRenderers[i].build());
chartState.markChartDirty();
}
}
}
common.Performance.timeEnd('chartsUpdateRenderers');
common.Performance.time('chartsUpdateBehaviors');
_updateBehaviors(chart, chartState);
common.Performance.timeEnd('chartsUpdateBehaviors');
_updateSelectionModel(chart, chartState);
chart.transition = animate ? animationDuration : Duration.zero;
}
void _updateBehaviors(common.BaseChart chart, BaseChartState chartState) {
final behaviorList = behaviors != null
? new List<ChartBehavior>.from(behaviors)
: <ChartBehavior>[];
// Insert automatic behaviors to the front of the behavior list.
if (defaultInteractions) {
if (chartState.autoBehaviorWidgets.isEmpty) {
addDefaultInteractions(chartState.autoBehaviorWidgets);
}
// Add default interaction behaviors to the front of the list if they
// don't conflict with user behaviors by role.
chartState.autoBehaviorWidgets.reversed
.where(_notACustomBehavior)
.forEach((ChartBehavior behavior) {
behaviorList.insert(0, behavior);
});
}
// Remove any behaviors from the chart that are not in the incoming list.
// Walk in reverse order they were added.
// Also, remove any persisting behaviors from incoming list.
for (int i = chartState.addedBehaviorWidgets.length - 1; i >= 0; i--) {
final addedBehavior = chartState.addedBehaviorWidgets[i];
if (!behaviorList.remove(addedBehavior)) {
final role = addedBehavior.role;
chartState.addedBehaviorWidgets.remove(addedBehavior);
chartState.addedCommonBehaviorsByRole.remove(role);
chart.removeBehavior(chartState.addedCommonBehaviorsByRole[role]);
chartState.markChartDirty();
}
}
// Add any remaining/new behaviors.
behaviorList.forEach((ChartBehavior behaviorWidget) {
final commonBehavior = chart
.createBehavior(<D>() => behaviorWidget.createCommonBehavior<D>());
// Assign the chart state to any behavior that needs it.
if (commonBehavior is ChartStateBehavior) {
(commonBehavior as ChartStateBehavior).chartState = chartState;
}
chart.addBehavior(commonBehavior);
chartState.addedBehaviorWidgets.add(behaviorWidget);
chartState.addedCommonBehaviorsByRole[behaviorWidget.role] =
commonBehavior;
chartState.markChartDirty();
});
}
/// Create the list of default interaction behaviors.
void addDefaultInteractions(List<ChartBehavior> behaviors) {
// Update selection model
behaviors.add(new SelectNearest(
eventTrigger: common.SelectionTrigger.tap,
selectionModelType: common.SelectionModelType.info,
expandToDomain: true,
selectClosestSeries: true));
}
bool _notACustomBehavior(ChartBehavior behavior) {
return this.behaviors == null ||
!this.behaviors.any(
(ChartBehavior userBehavior) => userBehavior.role == behavior.role);
}
void _updateSelectionModel(
common.BaseChart<D> chart, BaseChartState<D> chartState) {
final prevTypes = new List<common.SelectionModelType>.from(
chartState.addedSelectionChangedListenersByType.keys);
// Update any listeners for each type.
selectionModels?.forEach((SelectionModelConfig<D> model) {
final selectionModel = chart.getSelectionModel(model.type);
final prevChangedListener =
chartState.addedSelectionChangedListenersByType[model.type];
if (!identical(model.changedListener, prevChangedListener)) {
selectionModel.removeSelectionChangedListener(prevChangedListener);
selectionModel.addSelectionChangedListener(model.changedListener);
chartState.addedSelectionChangedListenersByType[model.type] =
model.changedListener;
}
final prevUpdatedListener =
chartState.addedSelectionUpdatedListenersByType[model.type];
if (!identical(model.updatedListener, prevUpdatedListener)) {
selectionModel.removeSelectionUpdatedListener(prevUpdatedListener);
selectionModel.addSelectionUpdatedListener(model.updatedListener);
chartState.addedSelectionUpdatedListenersByType[model.type] =
model.updatedListener;
}
prevTypes.remove(model.type);
});
// Remove any lingering listeners.
prevTypes.forEach((common.SelectionModelType type) {
chart.getSelectionModel(type)
..removeSelectionChangedListener(
chartState.addedSelectionChangedListenersByType[type])
..removeSelectionUpdatedListener(
chartState.addedSelectionUpdatedListenersByType[type]);
});
}
/// Gets distinct set of gestures this chart will subscribe to.
///
/// This is needed to allow setup of the [GestureDetector] widget with only
/// gestures we need to listen to and it must wrap [ChartContainer] widget.
/// Gestures are then setup to be proxied in [common.BaseChart] and that is
/// held by [ChartContainerRenderObject].
Set<GestureType> getDesiredGestures(BaseChartState chartState) {
final types = new Set<GestureType>();
behaviors?.forEach((ChartBehavior behavior) {
types.addAll(behavior.desiredGestures);
});
if (defaultInteractions && chartState.autoBehaviorWidgets.isEmpty) {
addDefaultInteractions(chartState.autoBehaviorWidgets);
}
chartState.autoBehaviorWidgets.forEach((ChartBehavior behavior) {
types.addAll(behavior.desiredGestures);
});
return types;
}
}
@immutable
class LayoutConfig {
final common.MarginSpec leftMarginSpec;
final common.MarginSpec topMarginSpec;
final common.MarginSpec rightMarginSpec;
final common.MarginSpec bottomMarginSpec;
LayoutConfig({
@required this.leftMarginSpec,
@required this.topMarginSpec,
@required this.rightMarginSpec,
@required this.bottomMarginSpec,
});
common.LayoutConfig get commonLayoutConfig => new common.LayoutConfig(
leftSpec: leftMarginSpec,
topSpec: topMarginSpec,
rightSpec: rightMarginSpec,
bottomSpec: bottomMarginSpec);
}

View File

@@ -0,0 +1,179 @@
// 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:flutter_web_ui/ui.dart' show TextDirection;
import 'package:flutter_web/material.dart'
show
AnimationController,
BuildContext,
State,
TickerProviderStateMixin,
Widget;
import 'package:charts_common/common.dart' as common;
import 'package:flutter_web/widgets.dart'
show Directionality, LayoutId, CustomMultiChildLayout;
import 'behaviors/chart_behavior.dart'
show BuildableBehavior, ChartBehavior, ChartStateBehavior;
import 'base_chart.dart' show BaseChart;
import 'chart_container.dart' show ChartContainer;
import 'chart_state.dart' show ChartState;
import 'chart_gesture_detector.dart' show ChartGestureDetector;
import 'widget_layout_delegate.dart';
class BaseChartState<D> extends State<BaseChart<D>>
with TickerProviderStateMixin
implements ChartState {
// Animation
AnimationController _animationController;
double _animationValue = 0.0;
Widget _oldWidget;
ChartGestureDetector _chartGestureDetector;
bool _configurationChanged = false;
final autoBehaviorWidgets = <ChartBehavior>[];
final addedBehaviorWidgets = <ChartBehavior>[];
final addedCommonBehaviorsByRole = <String, common.ChartBehavior>{};
final addedSelectionChangedListenersByType =
<common.SelectionModelType, common.SelectionModelListener<D>>{};
final addedSelectionUpdatedListenersByType =
<common.SelectionModelType, common.SelectionModelListener<D>>{};
final _behaviorAnimationControllers =
<ChartStateBehavior, AnimationController>{};
static const chartContainerLayoutID = 'chartContainer';
@override
void initState() {
super.initState();
_animationController = new AnimationController(vsync: this)
..addListener(_animationTick);
}
@override
void requestRebuild() {
setState(() {});
}
@override
void markChartDirty() {
_configurationChanged = true;
}
@override
void resetChartDirtyFlag() {
_configurationChanged = false;
}
@override
bool get chartIsDirty => _configurationChanged;
/// Builds the common chart canvas widget.
Widget _buildChartContainer() {
final chartContainer = new ChartContainer<D>(
oldChartWidget: _oldWidget,
chartWidget: widget,
chartState: this,
animationValue: _animationValue,
rtl: Directionality.of(context) == TextDirection.rtl,
rtlSpec: widget.rtlSpec,
userManagedState: widget.userManagedState,
);
_oldWidget = widget;
final desiredGestures = widget.getDesiredGestures(this);
if (desiredGestures.isNotEmpty) {
_chartGestureDetector ??= new ChartGestureDetector();
return _chartGestureDetector.makeWidget(
context, chartContainer, desiredGestures);
} else {
return chartContainer;
}
}
@override
Widget build(BuildContext context) {
final chartWidgets = <LayoutId>[];
final idAndBehaviorMap = <String, BuildableBehavior>{};
// Add the common chart canvas widget.
chartWidgets.add(new LayoutId(
id: chartContainerLayoutID, child: _buildChartContainer()));
// Add widget for each behavior that can build widgets
addedCommonBehaviorsByRole.forEach((id, behavior) {
if (behavior is BuildableBehavior) {
assert(id != chartContainerLayoutID);
final buildableBehavior = behavior as BuildableBehavior;
idAndBehaviorMap[id] = buildableBehavior;
final widget = buildableBehavior.build(context);
chartWidgets.add(new LayoutId(id: id, child: widget));
}
});
final isRTL = Directionality.of(context) == TextDirection.rtl;
return new CustomMultiChildLayout(
delegate: new WidgetLayoutDelegate(
chartContainerLayoutID, idAndBehaviorMap, isRTL),
children: chartWidgets);
}
@override
void dispose() {
_animationController.dispose();
_behaviorAnimationControllers
.forEach((_, controller) => controller?.dispose());
_behaviorAnimationControllers.clear();
super.dispose();
}
@override
void setAnimation(Duration transition) {
_playAnimation(transition);
}
void _playAnimation(Duration duration) {
_animationController.duration = duration;
_animationController.forward(from: (duration == Duration.zero) ? 1.0 : 0.0);
_animationValue = _animationController.value;
}
void _animationTick() {
setState(() {
_animationValue = _animationController.value;
});
}
/// Get animation controller to be used by [behavior].
AnimationController getAnimationController(ChartStateBehavior behavior) {
_behaviorAnimationControllers[behavior] ??=
new AnimationController(vsync: this);
return _behaviorAnimationControllers[behavior];
}
/// Dispose of animation controller used by [behavior].
void disposeAnimationController(ChartStateBehavior behavior) {
final controller = _behaviorAnimationControllers.remove(behavior);
controller?.dispose();
}
}

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 'package:charts_common/common.dart' as common
show DomainA11yExploreBehavior, VocalizationCallback, ExploreModeTrigger;
import 'package:flutter_web/widgets.dart' show hashValues;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
/// Behavior that generates semantic nodes for each domain.
class DomainA11yExploreBehavior
extends ChartBehavior<common.DomainA11yExploreBehavior> {
/// Returns a string for a11y vocalization from a list of series datum.
final common.VocalizationCallback vocalizationCallback;
final Set<GestureType> desiredGestures;
/// The gesture that activates explore mode. Defaults to long press.
///
/// Turning on explore mode asks this [A11yBehavior] to generate nodes within
/// this chart.
final common.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;
DomainA11yExploreBehavior._internal(
{this.vocalizationCallback,
this.exploreModeTrigger,
this.desiredGestures,
this.minimumWidth,
this.exploreModeEnabledAnnouncement,
this.exploreModeDisabledAnnouncement});
factory DomainA11yExploreBehavior(
{common.VocalizationCallback vocalizationCallback,
common.ExploreModeTrigger exploreModeTrigger,
double minimumWidth,
String exploreModeEnabledAnnouncement,
String exploreModeDisabledAnnouncement}) {
final desiredGestures = new Set<GestureType>();
exploreModeTrigger ??= common.ExploreModeTrigger.pressHold;
switch (exploreModeTrigger) {
case common.ExploreModeTrigger.pressHold:
desiredGestures..add(GestureType.onLongPress);
break;
case common.ExploreModeTrigger.tap:
desiredGestures..add(GestureType.onTap);
break;
}
return new DomainA11yExploreBehavior._internal(
vocalizationCallback: vocalizationCallback,
desiredGestures: desiredGestures,
exploreModeTrigger: exploreModeTrigger,
minimumWidth: minimumWidth,
exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement,
exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement,
);
}
@override
common.DomainA11yExploreBehavior<D> createCommonBehavior<D>() {
return new common.DomainA11yExploreBehavior<D>(
vocalizationCallback: vocalizationCallback,
exploreModeTrigger: exploreModeTrigger,
minimumWidth: minimumWidth,
exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement,
exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement);
}
@override
void updateCommonBehavior(common.DomainA11yExploreBehavior commonBehavior) {}
@override
String get role => 'DomainA11yExplore-${exploreModeTrigger}';
@override
bool operator ==(Object o) =>
o is DomainA11yExploreBehavior &&
vocalizationCallback == o.vocalizationCallback &&
exploreModeTrigger == o.exploreModeTrigger &&
minimumWidth == o.minimumWidth &&
exploreModeEnabledAnnouncement == o.exploreModeEnabledAnnouncement &&
exploreModeDisabledAnnouncement == o.exploreModeDisabledAnnouncement;
@override
int get hashCode {
return hashValues(minimumWidth, vocalizationCallback, exploreModeTrigger,
exploreModeEnabledAnnouncement, exploreModeDisabledAnnouncement);
}
}

View File

@@ -0,0 +1,72 @@
// 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/common.dart' as common
show PercentInjector, PercentInjectorTotalType;
import 'package:meta/meta.dart' show immutable;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
/// 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.
@immutable
class PercentInjector extends ChartBehavior<common.PercentInjector> {
final desiredGestures = new Set<GestureType>();
/// The type of data total to be calculated.
final common.PercentInjectorTotalType totalType;
PercentInjector._internal({this.totalType});
/// Constructs a [PercentInjector].
///
/// [totalType] configures the type of data total to be calculated.
factory PercentInjector({common.PercentInjectorTotalType totalType}) {
totalType ??= common.PercentInjectorTotalType.domain;
return new PercentInjector._internal(totalType: totalType);
}
@override
common.PercentInjector<D> createCommonBehavior<D>() =>
new common.PercentInjector<D>(totalType: totalType);
@override
void updateCommonBehavior(common.PercentInjector commonBehavior) {}
@override
String get role => 'PercentInjector';
@override
bool operator ==(Object o) {
return o is PercentInjector && totalType == o.totalType;
}
@override
int get hashCode => totalType.hashCode;
}

View File

@@ -0,0 +1,72 @@
// 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:charts_common/common.dart' as common
show
BehaviorPosition,
InsideJustification,
OutsideJustification,
ChartBehavior;
import 'package:meta/meta.dart' show immutable;
import 'package:flutter_web/widgets.dart' show BuildContext, Widget;
import '../base_chart_state.dart' show BaseChartState;
/// Flutter wrapper for chart behaviors.
@immutable
abstract class ChartBehavior<B extends common.ChartBehavior> {
Set<GestureType> get desiredGestures;
B createCommonBehavior<D>();
void updateCommonBehavior(B commonBehavior);
String get role;
}
/// A chart behavior that depends on Flutter [State].
abstract class ChartStateBehavior<B extends common.ChartBehavior> {
set chartState(BaseChartState chartState);
}
/// A chart behavior that can build a Flutter [Widget].
abstract class BuildableBehavior<B extends common.ChartBehavior> {
/// Builds a [Widget] based on the information passed in.
///
/// [context] Flutter build context for extracting inherited properties such
/// as Directionality.
Widget build(BuildContext context);
/// The position on the widget.
common.BehaviorPosition get position;
/// Justification of the widget, if [position] is top, bottom, start, or end.
common.OutsideJustification get outsideJustification;
/// Justification of the widget if [position] is [common.BehaviorPosition.inside].
common.InsideJustification get insideJustification;
/// Chart's draw area bounds are used for positioning.
Rectangle<int> get drawAreaBounds;
}
/// Types of gestures accepted by a chart.
enum GestureType {
onLongPress,
onTap,
onHover,
onDrag,
}

View File

@@ -0,0 +1,200 @@
// 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/common.dart' as common
show
BehaviorPosition,
ChartTitle,
ChartTitleDirection,
MaxWidthStrategy,
OutsideJustification,
TextStyleSpec;
import 'package:flutter_web/widgets.dart' show hashValues;
import 'package:meta/meta.dart' show immutable;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that adds a ChartTitle widget to a chart.
@immutable
class ChartTitle extends ChartBehavior<common.ChartTitle> {
final desiredGestures = new Set<GestureType>();
final common.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.
final int 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.
final int layoutPreferredSize;
/// Strategy for handling title text that is too large to fit. Defaults to
/// truncating the text with ellipses.
final common.MaxWidthStrategy maxWidthStrategy;
/// Primary text for the title.
final String 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].
final common.ChartTitleDirection titleDirection;
/// Justification of the title text if it is positioned outside of the draw
/// area.
final common.OutsideJustification titleOutsideJustification;
/// Space between the title and sub-title text, if defined.
///
/// This padding is not used if no sub-title is provided.
final int titlePadding;
/// Style of the [title] text.
final common.TextStyleSpec titleStyleSpec;
/// Secondary text for the sub-title.
///
/// [subTitle] is rendered on a second line below the [title], and may be
/// styled differently.
final String subTitle;
/// Style of the [subTitle] text.
final common.TextStyleSpec 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.
final int 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.
final int outerPadding;
/// Constructs a [ChartTitle].
///
/// [title] primary text for the title.
///
/// [behaviorPosition] layout position for the title. Defaults to the top of
/// the chart.
///
/// [innerPadding] space between the "inside" of the chart, and the title
/// behavior itself.
///
/// [maxWidthStrategy] strategy for handling title text that is too large to
/// fit. Defaults to truncating the text with ellipses.
///
/// [titleDirection] direction of the chart title text.
///
/// [titleOutsideJustification] Justification of the title text if it is
/// positioned outside of the draw. Defaults to the middle of the margin area.
///
/// [titlePadding] space between the title and sub-title text, if defined.
///
/// [titleStyleSpec] style of the [title] text.
///
/// [subTitle] secondary text for the sub-title. Optional.
///
/// [subTitleStyleSpec] style of the [subTitle] text.
ChartTitle(this.title,
{this.behaviorPosition,
this.innerPadding,
this.layoutMinSize,
this.layoutPreferredSize,
this.outerPadding,
this.maxWidthStrategy,
this.titleDirection,
this.titleOutsideJustification,
this.titlePadding,
this.titleStyleSpec,
this.subTitle,
this.subTitleStyleSpec});
@override
common.ChartTitle<D> createCommonBehavior<D>() =>
new common.ChartTitle<D>(title,
behaviorPosition: behaviorPosition,
innerPadding: innerPadding,
layoutMinSize: layoutMinSize,
layoutPreferredSize: layoutPreferredSize,
outerPadding: outerPadding,
maxWidthStrategy: maxWidthStrategy,
titleDirection: titleDirection,
titleOutsideJustification: titleOutsideJustification,
titlePadding: titlePadding,
titleStyleSpec: titleStyleSpec,
subTitle: subTitle,
subTitleStyleSpec: subTitleStyleSpec);
@override
void updateCommonBehavior(common.ChartTitle commonBehavior) {}
@override
String get role => 'ChartTitle-${behaviorPosition.toString()}';
@override
bool operator ==(Object o) {
return o is ChartTitle &&
behaviorPosition == o.behaviorPosition &&
layoutMinSize == o.layoutMinSize &&
layoutPreferredSize == o.layoutPreferredSize &&
maxWidthStrategy == o.maxWidthStrategy &&
title == o.title &&
titleDirection == o.titleDirection &&
titleOutsideJustification == o.titleOutsideJustification &&
titleStyleSpec == o.titleStyleSpec &&
subTitle == o.subTitle &&
subTitleStyleSpec == o.subTitleStyleSpec &&
innerPadding == o.innerPadding &&
titlePadding == o.titlePadding &&
outerPadding == o.outerPadding;
}
@override
int get hashCode {
return hashValues(
behaviorPosition,
layoutMinSize,
layoutPreferredSize,
maxWidthStrategy,
title,
titleDirection,
titleOutsideJustification,
titleStyleSpec,
subTitle,
subTitleStyleSpec,
innerPadding,
titlePadding,
outerPadding);
}
}

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:charts_common/common.dart' as common
show DomainHighlighter, SelectionModelType;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// 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.
@immutable
class DomainHighlighter extends ChartBehavior<common.DomainHighlighter> {
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
DomainHighlighter([this.selectionModelType = common.SelectionModelType.info]);
@override
common.DomainHighlighter<D> createCommonBehavior<D>() =>
new common.DomainHighlighter<D>(selectionModelType);
@override
void updateCommonBehavior(common.DomainHighlighter commonBehavior) {}
@override
String get role => 'domainHighlight-${selectionModelType.toString()}';
@override
bool operator ==(Object o) =>
o is DomainHighlighter && selectionModelType == o.selectionModelType;
@override
int get hashCode => selectionModelType.hashCode;
}

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 'package:collection/collection.dart' show ListEquality;
import 'package:charts_common/common.dart' as common
show InitialSelection, SeriesDatumConfig, SelectionModelType;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that sets the initial selection for a [selectionModelType].
@immutable
class InitialSelection extends ChartBehavior<common.InitialSelection> {
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
final List<String> selectedSeriesConfig;
final List<common.SeriesDatumConfig> selectedDataConfig;
InitialSelection(
{this.selectionModelType = common.SelectionModelType.info,
this.selectedSeriesConfig,
this.selectedDataConfig});
@override
common.InitialSelection<D> createCommonBehavior<D>() =>
new common.InitialSelection<D>(
selectionModelType: selectionModelType,
selectedDataConfig: selectedDataConfig,
selectedSeriesConfig: selectedSeriesConfig);
@override
void updateCommonBehavior(common.InitialSelection commonBehavior) {}
@override
String get role => 'InitialSelection-${selectionModelType.toString()}';
@override
bool operator ==(Object o) {
return o is InitialSelection &&
selectionModelType == o.selectionModelType &&
new ListEquality()
.equals(selectedSeriesConfig, o.selectedSeriesConfig) &&
new ListEquality().equals(selectedDataConfig, o.selectedDataConfig);
}
@override
int get hashCode {
int hashcode = selectionModelType.hashCode;
hashcode = hashcode * 37 + (selectedSeriesConfig?.hashCode ?? 0);
hashcode = hashcode * 37 + (selectedDataConfig?.hashCode ?? 0);
return hashcode;
}
}

View File

@@ -0,0 +1,340 @@
// 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/common.dart' as common
show
BehaviorPosition,
DatumLegend,
InsideJustification,
LegendEntry,
MeasureFormatter,
LegendDefaultMeasure,
OutsideJustification,
SelectionModelType,
TextStyleSpec;
import 'package:flutter_web/widgets.dart'
show BuildContext, EdgeInsets, Widget, hashValues;
import 'package:meta/meta.dart' show immutable;
import '../../chart_container.dart' show ChartContainerRenderObject;
import '../chart_behavior.dart'
show BuildableBehavior, ChartBehavior, GestureType;
import 'legend.dart' show TappableLegend;
import 'legend_content_builder.dart'
show LegendContentBuilder, TabularLegendContentBuilder;
import 'legend_layout.dart' show TabularLegendLayout;
/// Datum legend behavior for charts.
///
/// By default this behavior creates one legend entry per datum in the first
/// series rendered on the chart.
@immutable
class DatumLegend extends ChartBehavior<common.DatumLegend> {
static const defaultBehaviorPosition = common.BehaviorPosition.top;
static const defaultOutsideJustification =
common.OutsideJustification.startDrawArea;
static const defaultInsideJustification = common.InsideJustification.topStart;
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
/// Builder for creating custom legend content.
final LegendContentBuilder contentBuilder;
/// Position of the legend relative to the chart.
final common.BehaviorPosition position;
/// Justification of the legend relative to the chart
final common.OutsideJustification outsideJustification;
final common.InsideJustification insideJustification;
/// 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.
///
/// This flag is used by the [contentBuilder], so a custom content builder
/// has to choose if it wants to use this flag.
final bool showMeasures;
/// 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.
final common.LegendDefaultMeasure legendDefaultMeasure;
/// Formatter for measure value(s) if the measures are shown on the legend.
final common.MeasureFormatter measureFormatter;
/// Formatter for secondary measure value(s) if the measures are shown on the
/// legend and the series uses the secondary axis.
final common.MeasureFormatter secondaryMeasureFormatter;
/// Styles for legend entry label text.
final common.TextStyleSpec entryTextStyle;
static const defaultCellPadding = const EdgeInsets.all(8.0);
/// Create a new tabular layout legend.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [horizontalFirst] if true, legend entries will grow horizontally first
/// instead of vertically first. If the position is top, bottom, or inside,
/// this defaults to true. Otherwise false.
///
/// [desiredMaxRows] the max rows to use before layout out items in a new
/// column. By default there is no limit. The max columns created is the
/// smaller of desiredMaxRows and number of legend entries.
///
/// [desiredMaxColumns] the max columns to use before laying out items in a
/// new row. By default there is no limit. The max columns created is the
/// smaller of desiredMaxColumns and number of legend entries.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory DatumLegend({
common.BehaviorPosition position,
common.OutsideJustification outsideJustification,
common.InsideJustification insideJustification,
bool horizontalFirst,
int desiredMaxRows,
int desiredMaxColumns,
EdgeInsets cellPadding,
bool showMeasures,
common.LegendDefaultMeasure legendDefaultMeasure,
common.MeasureFormatter measureFormatter,
common.MeasureFormatter secondaryMeasureFormatter,
common.TextStyleSpec entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
cellPadding ??= defaultCellPadding;
// Set the tabular layout settings to match the position if it is not
// specified.
horizontalFirst ??= (position == common.BehaviorPosition.top ||
position == common.BehaviorPosition.bottom ||
position == common.BehaviorPosition.inside);
final layoutBuilder = horizontalFirst
? new TabularLegendLayout.horizontalFirst(
desiredMaxColumns: desiredMaxColumns, cellPadding: cellPadding)
: new TabularLegendLayout.verticalFirst(
desiredMaxRows: desiredMaxRows, cellPadding: cellPadding);
return new DatumLegend._internal(
contentBuilder:
new TabularLegendContentBuilder(legendLayout: layoutBuilder),
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle);
}
/// Create a legend with custom layout.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [contentBuilder] builder for the custom layout.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory DatumLegend.customLayout(
LegendContentBuilder contentBuilder, {
common.BehaviorPosition position,
common.OutsideJustification outsideJustification,
common.InsideJustification insideJustification,
bool showMeasures,
common.LegendDefaultMeasure legendDefaultMeasure,
common.MeasureFormatter measureFormatter,
common.MeasureFormatter secondaryMeasureFormatter,
common.TextStyleSpec entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
return new DatumLegend._internal(
contentBuilder: contentBuilder,
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle,
);
}
DatumLegend._internal({
this.contentBuilder,
this.selectionModelType,
this.position,
this.outsideJustification,
this.insideJustification,
this.showMeasures,
this.legendDefaultMeasure,
this.measureFormatter,
this.secondaryMeasureFormatter,
this.entryTextStyle,
});
@override
common.DatumLegend<D> createCommonBehavior<D>() =>
new _FlutterDatumLegend<D>(this);
@override
void updateCommonBehavior(common.DatumLegend commonBehavior) {
(commonBehavior as _FlutterDatumLegend).config = this;
}
/// All Legend behaviors get the same role ID, because you should only have
/// one legend on a chart.
@override
String get role => 'legend';
@override
bool operator ==(Object o) {
return o is DatumLegend &&
selectionModelType == o.selectionModelType &&
contentBuilder == o.contentBuilder &&
position == o.position &&
outsideJustification == o.outsideJustification &&
insideJustification == o.insideJustification &&
showMeasures == o.showMeasures &&
legendDefaultMeasure == o.legendDefaultMeasure &&
measureFormatter == o.measureFormatter &&
secondaryMeasureFormatter == o.secondaryMeasureFormatter &&
entryTextStyle == o.entryTextStyle;
}
@override
int get hashCode {
return hashValues(
selectionModelType,
contentBuilder,
position,
outsideJustification,
insideJustification,
showMeasures,
legendDefaultMeasure,
measureFormatter,
secondaryMeasureFormatter,
entryTextStyle);
}
}
/// Flutter specific wrapper on the common Legend for building content.
class _FlutterDatumLegend<D> extends common.DatumLegend<D>
implements BuildableBehavior, TappableLegend {
DatumLegend config;
_FlutterDatumLegend(this.config)
: super(
selectionModelType: config.selectionModelType,
measureFormatter: config.measureFormatter,
secondaryMeasureFormatter: config.secondaryMeasureFormatter,
legendDefaultMeasure: config.legendDefaultMeasure,
) {
super.entryTextStyle = config.entryTextStyle;
}
@override
void updateLegend() {
(chartContext as ChartContainerRenderObject).requestRebuild();
}
@override
common.BehaviorPosition get position => config.position;
@override
common.OutsideJustification get outsideJustification =>
config.outsideJustification;
@override
common.InsideJustification get insideJustification =>
config.insideJustification;
@override
Widget build(BuildContext context) {
final hasSelection =
legendState.legendEntries.any((entry) => entry.isSelected);
// Show measures if [showMeasures] is true and there is a selection or if
// showing measures when there is no selection.
final showMeasures = config.showMeasures &&
(hasSelection ||
legendDefaultMeasure != common.LegendDefaultMeasure.none);
return config.contentBuilder
.build(context, legendState, this, showMeasures: showMeasures);
}
/// TODO: Maybe highlight the pie wedge.
@override
onLegendEntryTapUp(common.LegendEntry detail) {}
}

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.
import 'package:charts_common/common.dart' show LegendEntry, LegendTapHandling;
abstract class TappableLegend<T, D> {
/// Delegates handling of legend entry clicks according to the configured
/// [LegendTapHandling] strategy.
onLegendEntryTapUp(LegendEntry detail);
}

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 'package:charts_common/common.dart' as common
show Legend, LegendState, SeriesLegend;
import 'package:flutter_web/widgets.dart' show BuildContext, hashValues, Widget;
import 'legend.dart';
import 'legend_entry_layout.dart';
import 'legend_layout.dart';
/// Strategy for building a legend content widget.
abstract class LegendContentBuilder {
const LegendContentBuilder();
Widget build(BuildContext context, common.LegendState legendState,
common.Legend legend,
{bool showMeasures});
}
/// Base strategy for building a legend content widget.
///
/// Each legend entry is passed to a [LegendLayout] strategy to create a widget
/// for each legend entry. These widgets are then passed to a
/// [LegendEntryLayout] strategy to create the legend widget.
abstract class BaseLegendContentBuilder implements LegendContentBuilder {
/// Strategy for creating one widget or each legend entry.
LegendEntryLayout get legendEntryLayout;
/// Strategy for creating the legend content widget from a list of widgets.
///
/// This is typically the list of widgets from legend entries.
LegendLayout get legendLayout;
@override
Widget build(BuildContext context, common.LegendState legendState,
common.Legend legend,
{bool showMeasures}) {
final entryWidgets = legendState.legendEntries.map((entry) {
var isHidden = false;
if (legend is common.SeriesLegend) {
isHidden = legend.isSeriesHidden(entry.series.id);
}
return legendEntryLayout.build(
context, entry, legend as TappableLegend, isHidden,
showMeasures: showMeasures);
}).toList();
return legendLayout.build(context, entryWidgets);
}
}
// TODO: Expose settings for tabular layout.
/// Strategy that builds a tabular legend.
///
/// [legendEntryLayout] custom strategy for creating widgets for each legend
/// entry.
/// [legendLayout] custom strategy for creating legend widget from list of
/// widgets that represent a legend entry.
class TabularLegendContentBuilder extends BaseLegendContentBuilder {
final LegendEntryLayout legendEntryLayout;
final LegendLayout legendLayout;
TabularLegendContentBuilder(
{LegendEntryLayout legendEntryLayout, LegendLayout legendLayout})
: this.legendEntryLayout =
legendEntryLayout ?? const SimpleLegendEntryLayout(),
this.legendLayout =
legendLayout ?? new TabularLegendLayout.horizontalFirst();
@override
bool operator ==(Object o) {
return o is TabularLegendContentBuilder &&
legendEntryLayout == o.legendEntryLayout &&
legendLayout == o.legendLayout;
}
@override
int get hashCode => hashValues(legendEntryLayout, legendLayout);
}

View File

@@ -0,0 +1,144 @@
// 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/common.dart' as common;
import 'package:charts_flutter/src/util/color.dart';
import 'package:flutter_web/widgets.dart';
import 'package:flutter_web/material.dart'
show GestureDetector, GestureTapUpCallback, TapUpDetails, Theme;
import '../../symbol_renderer.dart';
import 'legend.dart' show TappableLegend;
/// Strategy for building one widget from one [common.LegendEntry].
abstract class LegendEntryLayout {
Widget build(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden,
{bool showMeasures});
}
/// Builds one legend entry as a row with symbol and label from the series.
///
/// If directionality from the chart context indicates RTL, the symbol is placed
/// to the right of the text instead of the left of the text.
class SimpleLegendEntryLayout implements LegendEntryLayout {
const SimpleLegendEntryLayout();
Widget createSymbol(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden) {
// TODO: Consider allowing scaling the size for the symbol.
// A custom symbol renderer can ignore this size and use their own.
final materialSymbolSize = new Size(12.0, 12.0);
final entryColor = legendEntry.color;
var color = ColorUtil.toDartColor(entryColor);
// Get the SymbolRendererBuilder wrapping a common.SymbolRenderer if needed.
final SymbolRendererBuilder symbolRendererBuilder =
legendEntry.symbolRenderer is SymbolRendererBuilder
? legendEntry.symbolRenderer
: new SymbolRendererCanvas(legendEntry.symbolRenderer);
return new GestureDetector(
child: symbolRendererBuilder.build(
context,
size: materialSymbolSize,
color: color,
enabled: !isHidden,
),
onTapUp: makeTapUpCallback(context, legendEntry, legend));
}
Widget createLabel(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden) {
TextStyle style =
_convertTextStyle(isHidden, context, legendEntry.textStyle);
return new GestureDetector(
child: new Text(legendEntry.label, style: style),
onTapUp: makeTapUpCallback(context, legendEntry, legend));
}
Widget createMeasureValue(BuildContext context,
common.LegendEntry legendEntry, TappableLegend legend, bool isHidden) {
return new GestureDetector(
child: new Text(legendEntry.formattedValue),
onTapUp: makeTapUpCallback(context, legendEntry, legend));
}
@override
Widget build(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden,
{bool showMeasures}) {
final rowChildren = <Widget>[];
// TODO: Allow setting to configure the padding.
final padding = new EdgeInsets.only(right: 8.0); // Material default.
final symbol = createSymbol(context, legendEntry, legend, isHidden);
final label = createLabel(context, legendEntry, legend, isHidden);
final measure = showMeasures
? createMeasureValue(context, legendEntry, legend, isHidden)
: null;
rowChildren.add(symbol);
rowChildren.add(new Container(padding: padding));
rowChildren.add(label);
if (measure != null) {
rowChildren.add(new Container(padding: padding));
rowChildren.add(measure);
}
// Row automatically reverses the content if Directionality is rtl.
return new Row(children: rowChildren);
}
GestureTapUpCallback makeTapUpCallback(BuildContext context,
common.LegendEntry legendEntry, TappableLegend legend) {
return (TapUpDetails d) {
legend.onLegendEntryTapUp(legendEntry);
};
}
bool operator ==(Object other) => other is SimpleLegendEntryLayout;
int get hashCode {
return this.runtimeType.hashCode;
}
/// Convert the charts common TextStlyeSpec into a standard TextStyle, while
/// reducing the color opacity to 26% if the entry is hidden.
///
/// For non-specified values, override the hidden text color to use the body 1
/// theme, but allow other properties of [Text] to be inherited.
TextStyle _convertTextStyle(
bool isHidden, BuildContext context, common.TextStyleSpec textStyle) {
Color color = textStyle?.color != null
? ColorUtil.toDartColor(textStyle.color)
: null;
if (isHidden) {
// Use a default color for hidden legend entries if none is provided.
color ??= Theme.of(context).textTheme.body1.color;
color = color.withOpacity(0.26);
}
return new TextStyle(
inherit: true,
fontFamily: textStyle?.fontFamily,
fontSize:
textStyle?.fontSize != null ? textStyle.fontSize.toDouble() : null,
color: color);
}
}

View File

@@ -0,0 +1,158 @@
// 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;
import 'package:flutter_web/rendering.dart';
import 'package:flutter_web/widgets.dart';
/// Strategy for building legend from legend entry widgets.
abstract class LegendLayout {
Widget build(BuildContext context, List<Widget> legendEntryWidgets);
}
/// Layout legend entries in tabular format.
class TabularLegendLayout implements LegendLayout {
/// No limit for max rows or max columns.
static const _noLimit = -1;
final bool isHorizontalFirst;
final int desiredMaxRows;
final int desiredMaxColumns;
final EdgeInsets cellPadding;
TabularLegendLayout._internal(
{this.isHorizontalFirst,
this.desiredMaxRows,
this.desiredMaxColumns,
this.cellPadding});
/// Layout horizontally until columns exceed [desiredMaxColumns].
///
/// [desiredMaxColumns] the max columns to use before laying out items in a
/// new row. By default there is no limit. The max columns created is the
/// smaller of desiredMaxColumns and number of legend entries.
///
/// [cellPadding] the [EdgeInsets] for each widget.
factory TabularLegendLayout.horizontalFirst({
int desiredMaxColumns,
EdgeInsets cellPadding,
}) {
return new TabularLegendLayout._internal(
isHorizontalFirst: true,
desiredMaxRows: _noLimit,
desiredMaxColumns: desiredMaxColumns ?? _noLimit,
cellPadding: cellPadding,
);
}
/// Layout vertically, until rows exceed [desiredMaxRows].
///
/// [desiredMaxRows] the max rows to use before layout out items in a new
/// column. By default there is no limit. The max columns created is the
/// smaller of desiredMaxRows and number of legend entries.
///
/// [cellPadding] the [EdgeInsets] for each widget.
factory TabularLegendLayout.verticalFirst({
int desiredMaxRows,
EdgeInsets cellPadding,
}) {
return new TabularLegendLayout._internal(
isHorizontalFirst: false,
desiredMaxRows: desiredMaxRows ?? _noLimit,
desiredMaxColumns: _noLimit,
cellPadding: cellPadding,
);
}
@override
Widget build(BuildContext context, List<Widget> legendEntries) {
final paddedLegendEntries = ((cellPadding == null)
? legendEntries
: legendEntries
.map((entry) => new Padding(padding: cellPadding, child: entry))
.toList());
return isHorizontalFirst
? _buildHorizontalFirst(paddedLegendEntries)
: _buildVerticalFirst(paddedLegendEntries);
}
@override
bool operator ==(o) =>
o is TabularLegendLayout &&
desiredMaxRows == o.desiredMaxRows &&
desiredMaxColumns == o.desiredMaxColumns &&
isHorizontalFirst == o.isHorizontalFirst &&
cellPadding == o.cellPadding;
@override
int get hashCode => hashValues(
desiredMaxRows, desiredMaxColumns, isHorizontalFirst, cellPadding);
Widget _buildHorizontalFirst(List<Widget> legendEntries) {
final maxColumns = (desiredMaxColumns == _noLimit)
? legendEntries.length
: min(legendEntries.length, desiredMaxColumns);
final rows = <TableRow>[];
for (var i = 0; i < legendEntries.length; i += maxColumns) {
rows.add(new TableRow(
children: legendEntries
.sublist(i, min(i + maxColumns, legendEntries.length))
.toList()));
}
return _buildTableFromRows(rows);
}
Widget _buildVerticalFirst(List<Widget> legendEntries) {
final maxRows = (desiredMaxRows == _noLimit)
? legendEntries.length
: min(legendEntries.length, desiredMaxRows);
final rows =
new List.generate(maxRows, (_) => new TableRow(children: <Widget>[]));
for (var i = 0; i < legendEntries.length; i++) {
rows[i % maxRows].children.add(legendEntries[i]);
}
return _buildTableFromRows(rows);
}
Table _buildTableFromRows(List<TableRow> rows) {
final padWidget = new Row();
// Pad rows to the max column count, because each TableRow in a table is
// required to have the same number of children.
final columnCount = rows
.map((r) => r.children.length)
.fold(0, (max, current) => (current > max) ? current : max);
for (var i = 0; i < rows.length; i++) {
final rowChildren = rows[i].children;
final padCount = columnCount - rowChildren.length;
if (padCount > 0) {
rowChildren.addAll(new Iterable.generate(padCount, (_) => padWidget));
}
}
// TODO: Investigate other means of creating the tabular legend
// Sizing the column width using [IntrinsicColumnWidth] is expensive per
// Flutter's documentation, but has to be used if the table is desired to
// have a width that is tight on each column.
return new Table(
children: rows, defaultColumnWidth: new IntrinsicColumnWidth());
}
}

View File

@@ -0,0 +1,382 @@
// 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/common.dart' as common
show
BehaviorPosition,
InsideJustification,
LegendEntry,
LegendTapHandling,
MeasureFormatter,
LegendDefaultMeasure,
OutsideJustification,
SeriesLegend,
SelectionModelType,
TextStyleSpec;
import 'package:collection/collection.dart' show ListEquality;
import 'package:flutter_web/widgets.dart'
show BuildContext, EdgeInsets, Widget, hashValues;
import 'package:meta/meta.dart' show immutable;
import '../../chart_container.dart' show ChartContainerRenderObject;
import '../chart_behavior.dart'
show BuildableBehavior, ChartBehavior, GestureType;
import 'legend.dart' show TappableLegend;
import 'legend_content_builder.dart'
show LegendContentBuilder, TabularLegendContentBuilder;
import 'legend_layout.dart' show TabularLegendLayout;
/// Series legend behavior for charts.
@immutable
class SeriesLegend extends ChartBehavior<common.SeriesLegend> {
static const defaultBehaviorPosition = common.BehaviorPosition.top;
static const defaultOutsideJustification =
common.OutsideJustification.startDrawArea;
static const defaultInsideJustification = common.InsideJustification.topStart;
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
/// Builder for creating custom legend content.
final LegendContentBuilder contentBuilder;
/// Position of the legend relative to the chart.
final common.BehaviorPosition position;
/// Justification of the legend relative to the chart
final common.OutsideJustification outsideJustification;
final common.InsideJustification insideJustification;
/// 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.
///
/// This flag is used by the [contentBuilder], so a custom content builder
/// has to choose if it wants to use this flag.
final bool showMeasures;
/// 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.
final common.LegendDefaultMeasure legendDefaultMeasure;
/// Formatter for measure value(s) if the measures are shown on the legend.
final common.MeasureFormatter measureFormatter;
/// Formatter for secondary measure value(s) if the measures are shown on the
/// legend and the series uses the secondary axis.
final common.MeasureFormatter secondaryMeasureFormatter;
/// Styles for legend entry label text.
final common.TextStyleSpec entryTextStyle;
static const defaultCellPadding = const EdgeInsets.all(8.0);
final List<String> defaultHiddenSeries;
/// Create a new tabular layout legend.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [horizontalFirst] if true, legend entries will grow horizontally first
/// instead of vertically first. If the position is top, bottom, or inside,
/// this defaults to true. Otherwise false.
///
/// [desiredMaxRows] the max rows to use before layout out items in a new
/// column. By default there is no limit. The max columns created is the
/// smaller of desiredMaxRows and number of legend entries.
///
/// [desiredMaxColumns] the max columns to use before laying out items in a
/// new row. By default there is no limit. The max columns created is the
/// smaller of desiredMaxColumns and number of legend entries.
///
/// [defaultHiddenSeries] lists the IDs of series that should be hidden on
/// first chart draw.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory SeriesLegend({
common.BehaviorPosition position,
common.OutsideJustification outsideJustification,
common.InsideJustification insideJustification,
bool horizontalFirst,
int desiredMaxRows,
int desiredMaxColumns,
EdgeInsets cellPadding,
List<String> defaultHiddenSeries,
bool showMeasures,
common.LegendDefaultMeasure legendDefaultMeasure,
common.MeasureFormatter measureFormatter,
common.MeasureFormatter secondaryMeasureFormatter,
common.TextStyleSpec entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
cellPadding ??= defaultCellPadding;
// Set the tabular layout settings to match the position if it is not
// specified.
horizontalFirst ??= (position == common.BehaviorPosition.top ||
position == common.BehaviorPosition.bottom ||
position == common.BehaviorPosition.inside);
final layoutBuilder = horizontalFirst
? new TabularLegendLayout.horizontalFirst(
desiredMaxColumns: desiredMaxColumns, cellPadding: cellPadding)
: new TabularLegendLayout.verticalFirst(
desiredMaxRows: desiredMaxRows, cellPadding: cellPadding);
return new SeriesLegend._internal(
contentBuilder:
new TabularLegendContentBuilder(legendLayout: layoutBuilder),
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
defaultHiddenSeries: defaultHiddenSeries,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle);
}
/// Create a legend with custom layout.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [contentBuilder] builder for the custom layout.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [defaultHiddenSeries] lists the IDs of series that should be hidden on
/// first chart draw.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory SeriesLegend.customLayout(
LegendContentBuilder contentBuilder, {
common.BehaviorPosition position,
common.OutsideJustification outsideJustification,
common.InsideJustification insideJustification,
List<String> defaultHiddenSeries,
bool showMeasures,
common.LegendDefaultMeasure legendDefaultMeasure,
common.MeasureFormatter measureFormatter,
common.MeasureFormatter secondaryMeasureFormatter,
common.TextStyleSpec entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
return new SeriesLegend._internal(
contentBuilder: contentBuilder,
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
defaultHiddenSeries: defaultHiddenSeries,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle,
);
}
SeriesLegend._internal({
this.contentBuilder,
this.selectionModelType,
this.position,
this.outsideJustification,
this.insideJustification,
this.defaultHiddenSeries,
this.showMeasures,
this.legendDefaultMeasure,
this.measureFormatter,
this.secondaryMeasureFormatter,
this.entryTextStyle,
});
@override
common.SeriesLegend<D> createCommonBehavior<D>() =>
new _FlutterSeriesLegend<D>(this);
@override
void updateCommonBehavior(common.SeriesLegend commonBehavior) {
(commonBehavior as _FlutterSeriesLegend).config = this;
}
/// All Legend behaviors get the same role ID, because you should only have
/// one legend on a chart.
@override
String get role => 'legend';
@override
bool operator ==(Object o) {
return o is SeriesLegend &&
selectionModelType == o.selectionModelType &&
contentBuilder == o.contentBuilder &&
position == o.position &&
outsideJustification == o.outsideJustification &&
insideJustification == o.insideJustification &&
new ListEquality().equals(defaultHiddenSeries, o.defaultHiddenSeries) &&
showMeasures == o.showMeasures &&
legendDefaultMeasure == o.legendDefaultMeasure &&
measureFormatter == o.measureFormatter &&
secondaryMeasureFormatter == o.secondaryMeasureFormatter &&
entryTextStyle == o.entryTextStyle;
}
@override
int get hashCode {
return hashValues(
selectionModelType,
contentBuilder,
position,
outsideJustification,
insideJustification,
defaultHiddenSeries,
showMeasures,
legendDefaultMeasure,
measureFormatter,
secondaryMeasureFormatter,
entryTextStyle);
}
}
/// Flutter specific wrapper on the common Legend for building content.
class _FlutterSeriesLegend<D> extends common.SeriesLegend<D>
implements BuildableBehavior, TappableLegend {
SeriesLegend config;
_FlutterSeriesLegend(this.config)
: super(
selectionModelType: config.selectionModelType,
measureFormatter: config.measureFormatter,
secondaryMeasureFormatter: config.secondaryMeasureFormatter,
legendDefaultMeasure: config.legendDefaultMeasure,
) {
super.defaultHiddenSeries = config.defaultHiddenSeries;
super.entryTextStyle = config.entryTextStyle;
}
@override
void updateLegend() {
(chartContext as ChartContainerRenderObject).requestRebuild();
}
@override
common.BehaviorPosition get position => config.position;
@override
common.OutsideJustification get outsideJustification =>
config.outsideJustification;
@override
common.InsideJustification get insideJustification =>
config.insideJustification;
@override
Widget build(BuildContext context) {
final hasSelection =
legendState.legendEntries.any((entry) => entry.isSelected);
// Show measures if [showMeasures] is true and there is a selection or if
// showing measures when there is no selection.
final showMeasures = config.showMeasures &&
(hasSelection ||
legendDefaultMeasure != common.LegendDefaultMeasure.none);
return config.contentBuilder
.build(context, legendState, this, showMeasures: showMeasures);
}
@override
onLegendEntryTapUp(common.LegendEntry detail) {
switch (legendTapHandling) {
case common.LegendTapHandling.hide:
_hideSeries(detail);
break;
case common.LegendTapHandling.none:
default:
break;
}
}
/// Handles tap events by hiding or un-hiding entries tapped in the legend.
///
/// Tapping on a visible series in the legend will hide it. Tapping on a
/// hidden series will make it visible again.
void _hideSeries(common.LegendEntry detail) {
final seriesId = detail.series.id;
// Handle the event by toggling the hidden state of the target.
if (isSeriesHidden(seriesId)) {
showSeries(seriesId);
} else {
hideSeries(seriesId);
}
// Redraw the chart to actually hide hidden series.
chart.redraw(skipLayout: true, skipAnimation: false);
}
}

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 'package:collection/collection.dart' show ListEquality;
import 'package:charts_common/common.dart' as common
show
LinePointHighlighter,
LinePointHighlighterFollowLineType,
SelectionModelType,
SymbolRenderer;
import 'package:flutter_web/widgets.dart' show hashValues;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// 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.
@immutable
class LinePointHighlighter extends ChartBehavior<common.LinePointHighlighter> {
final desiredGestures = new Set<GestureType>();
final common.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;
final common.LinePointHighlighterFollowLineType showHorizontalFollowLine;
final common.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 common.SymbolRenderer symbolRenderer;
LinePointHighlighter(
{this.selectionModelType,
this.defaultRadiusPx,
this.radiusPaddingPx,
this.showHorizontalFollowLine,
this.showVerticalFollowLine,
this.dashPattern,
this.drawFollowLinesAcrossChart,
this.symbolRenderer});
@override
common.LinePointHighlighter<D> createCommonBehavior<D>() =>
new common.LinePointHighlighter<D>(
selectionModelType: selectionModelType,
defaultRadiusPx: defaultRadiusPx,
radiusPaddingPx: radiusPaddingPx,
showHorizontalFollowLine: showHorizontalFollowLine,
showVerticalFollowLine: showVerticalFollowLine,
dashPattern: dashPattern,
drawFollowLinesAcrossChart: drawFollowLinesAcrossChart,
symbolRenderer: symbolRenderer,
);
@override
void updateCommonBehavior(common.LinePointHighlighter commonBehavior) {}
@override
String get role => 'LinePointHighlighter-${selectionModelType.toString()}';
@override
bool operator ==(Object o) {
return o is LinePointHighlighter &&
defaultRadiusPx == o.defaultRadiusPx &&
radiusPaddingPx == o.radiusPaddingPx &&
showHorizontalFollowLine == o.showHorizontalFollowLine &&
showVerticalFollowLine == o.showVerticalFollowLine &&
selectionModelType == o.selectionModelType &&
new ListEquality().equals(dashPattern, o.dashPattern) &&
drawFollowLinesAcrossChart == o.drawFollowLinesAcrossChart;
}
@override
int get hashCode {
return hashValues(
selectionModelType,
defaultRadiusPx,
radiusPaddingPx,
showHorizontalFollowLine,
showVerticalFollowLine,
dashPattern,
drawFollowLinesAcrossChart,
);
}
}

View File

@@ -0,0 +1,117 @@
// 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/common.dart' as common
show
AnnotationLabelAnchor,
AnnotationLabelDirection,
AnnotationLabelPosition,
AnnotationSegment,
Color,
MaterialPalette,
RangeAnnotation,
TextStyleSpec;
import 'package:collection/collection.dart' show ListEquality;
import 'package:flutter_web/widgets.dart' show hashValues;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that annotations domain ranges with a solid fill color.
///
/// The annotations will be drawn underneath series data and chart axes.
///
/// This is typically used for line charts to call out sections of the data
/// range.
@immutable
class RangeAnnotation extends ChartBehavior<common.RangeAnnotation> {
final desiredGestures = new Set<GestureType>();
/// List of annotations to render on the chart.
final List<common.AnnotationSegment> annotations;
/// Configures where to anchor annotation label text.
final common.AnnotationLabelAnchor defaultLabelAnchor;
/// Direction of label text on the annotations.
final common.AnnotationLabelDirection defaultLabelDirection;
/// Configures where to place labels relative to the annotation.
final common.AnnotationLabelPosition defaultLabelPosition;
/// Configures the style of label text.
final common.TextStyleSpec defaultLabelStyleSpec;
/// Default color for annotations.
final common.Color defaultColor;
/// Whether or not the range of the axis should be extended to include the
/// annotation start and end values.
final bool extendAxis;
/// Space before and after label text.
final int labelPadding;
RangeAnnotation(this.annotations,
{common.Color defaultColor,
this.defaultLabelAnchor,
this.defaultLabelDirection,
this.defaultLabelPosition,
this.defaultLabelStyleSpec,
this.extendAxis,
this.labelPadding})
: defaultColor = common.MaterialPalette.gray.shade100;
@override
common.RangeAnnotation<D> createCommonBehavior<D>() =>
new common.RangeAnnotation<D>(annotations,
defaultColor: defaultColor,
defaultLabelAnchor: defaultLabelAnchor,
defaultLabelDirection: defaultLabelDirection,
defaultLabelPosition: defaultLabelPosition,
defaultLabelStyleSpec: defaultLabelStyleSpec,
extendAxis: extendAxis,
labelPadding: labelPadding);
@override
void updateCommonBehavior(common.RangeAnnotation commonBehavior) {}
@override
String get role => 'RangeAnnotation';
@override
bool operator ==(Object o) {
return o is RangeAnnotation &&
new ListEquality().equals(annotations, o.annotations) &&
defaultColor == o.defaultColor &&
extendAxis == o.extendAxis &&
defaultLabelAnchor == o.defaultLabelAnchor &&
defaultLabelDirection == o.defaultLabelDirection &&
defaultLabelPosition == o.defaultLabelPosition &&
defaultLabelStyleSpec == o.defaultLabelStyleSpec &&
labelPadding == o.labelPadding;
}
@override
int get hashCode => hashValues(
annotations,
defaultColor,
extendAxis,
defaultLabelAnchor,
defaultLabelDirection,
defaultLabelPosition,
defaultLabelStyleSpec,
labelPadding);
}

View File

@@ -0,0 +1,147 @@
// 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/common.dart' as common
show ChartBehavior, SelectNearest, SelectionModelType, SelectionTrigger;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// 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 will be included in the selection. (Default: true)
/// selectClosestSeries - mark the series for the closest data point as
/// selected. (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.
@immutable
class SelectNearest extends ChartBehavior<common.SelectNearest> {
final Set<GestureType> desiredGestures;
final common.SelectionModelType selectionModelType;
final common.SelectionTrigger eventTrigger;
final bool expandToDomain;
final bool selectAcrossAllDrawAreaComponents;
final bool selectClosestSeries;
final int maximumDomainDistancePx;
SelectNearest._internal(
{this.selectionModelType,
this.expandToDomain = true,
this.selectAcrossAllDrawAreaComponents = false,
this.selectClosestSeries = true,
this.eventTrigger,
this.desiredGestures,
this.maximumDomainDistancePx});
factory SelectNearest(
{common.SelectionModelType selectionModelType =
common.SelectionModelType.info,
bool expandToDomain = true,
bool selectAcrossAllDrawAreaComponents = false,
bool selectClosestSeries = true,
common.SelectionTrigger eventTrigger = common.SelectionTrigger.tap,
int maximumDomainDistancePx}) {
return new SelectNearest._internal(
selectionModelType: selectionModelType,
expandToDomain: expandToDomain,
selectAcrossAllDrawAreaComponents: selectAcrossAllDrawAreaComponents,
selectClosestSeries: selectClosestSeries,
eventTrigger: eventTrigger,
desiredGestures: SelectNearest._getDesiredGestures(eventTrigger),
maximumDomainDistancePx: maximumDomainDistancePx);
}
static Set<GestureType> _getDesiredGestures(
common.SelectionTrigger eventTrigger) {
final desiredGestures = new Set<GestureType>();
switch (eventTrigger) {
case common.SelectionTrigger.tap:
desiredGestures..add(GestureType.onTap);
break;
case common.SelectionTrigger.tapAndDrag:
desiredGestures..add(GestureType.onTap)..add(GestureType.onDrag);
break;
case common.SelectionTrigger.pressHold:
case common.SelectionTrigger.longPressHold:
desiredGestures
..add(GestureType.onTap)
..add(GestureType.onLongPress)
..add(GestureType.onDrag);
break;
case common.SelectionTrigger.hover:
default:
desiredGestures..add(GestureType.onHover);
break;
}
return desiredGestures;
}
@override
common.SelectNearest<D> createCommonBehavior<D>() {
return new common.SelectNearest<D>(
selectionModelType: selectionModelType,
eventTrigger: eventTrigger,
expandToDomain: expandToDomain,
selectClosestSeries: selectClosestSeries,
maximumDomainDistancePx: maximumDomainDistancePx);
}
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {}
// TODO: Explore the performance impact of calculating this once
// at the constructor for this and common ChartBehaviors.
@override
String get role => 'SelectNearest-${selectionModelType.toString()}}';
bool operator ==(Object other) {
if (other is SelectNearest) {
return (selectionModelType == other.selectionModelType) &&
(eventTrigger == other.eventTrigger) &&
(expandToDomain == other.expandToDomain) &&
(selectClosestSeries == other.selectClosestSeries) &&
(maximumDomainDistancePx == other.maximumDomainDistancePx);
} else {
return false;
}
}
int get hashCode {
int hashcode = selectionModelType.hashCode;
hashcode = hashcode * 37 + eventTrigger.hashCode;
hashcode = hashcode * 37 + expandToDomain.hashCode;
hashcode = hashcode * 37 + selectClosestSeries.hashCode;
hashcode = hashcode * 37 + maximumDomainDistancePx.hashCode;
return hashcode;
}
}

View File

@@ -0,0 +1,196 @@
// 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:charts_common/common.dart' as common
show
LayoutViewPaintOrder,
RectSymbolRenderer,
SelectionTrigger,
Slider,
SliderListenerCallback,
SliderStyle,
SymbolRenderer;
import 'package:flutter_web/widgets.dart' show hashValues;
import 'package:meta/meta.dart' show immutable;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
/// 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.
@immutable
class Slider extends ChartBehavior<common.Slider> {
final Set<GestureType> desiredGestures;
/// 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 common.SelectionTrigger eventTrigger;
/// 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).
final int layoutPaintOrder;
/// Initial domain position of the slider, in domain units.
final dynamic initialDomainValue;
/// Callback function that will be called when the position of the slider
/// changes during a drag event.
///
/// The callback will be given the current domain position of the slider.
final common.SliderListenerCallback onChangeCallback;
/// Custom role ID for this slider
final 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.
final common.SliderStyle style;
/// Renderer for the handle. Defaults to a rectangle.
final common.SymbolRenderer handleRenderer;
Slider._internal(
{this.eventTrigger,
this.onChangeCallback,
this.initialDomainValue,
this.roleId,
this.snapToDatum,
this.style,
this.handleRenderer,
this.desiredGestures,
this.layoutPaintOrder});
/// 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.
///
/// [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).
factory Slider(
{common.SelectionTrigger eventTrigger,
common.SymbolRenderer handleRenderer,
dynamic initialDomainValue,
String roleId,
common.SliderListenerCallback onChangeCallback,
bool snapToDatum = false,
common.SliderStyle style,
int layoutPaintOrder = common.LayoutViewPaintOrder.slider}) {
eventTrigger ??= common.SelectionTrigger.tapAndDrag;
handleRenderer ??= new common.RectSymbolRenderer();
// Default the handle size large enough to tap on a mobile device.
style ??= new common.SliderStyle(handleSize: Rectangle<int>(0, 0, 20, 30));
return new Slider._internal(
eventTrigger: eventTrigger,
handleRenderer: handleRenderer,
initialDomainValue: initialDomainValue,
onChangeCallback: onChangeCallback,
roleId: roleId,
snapToDatum: snapToDatum,
style: style,
desiredGestures: Slider._getDesiredGestures(eventTrigger),
layoutPaintOrder: layoutPaintOrder);
}
static Set<GestureType> _getDesiredGestures(
common.SelectionTrigger eventTrigger) {
final desiredGestures = new Set<GestureType>();
switch (eventTrigger) {
case common.SelectionTrigger.tapAndDrag:
desiredGestures..add(GestureType.onTap)..add(GestureType.onDrag);
break;
case common.SelectionTrigger.pressHold:
case common.SelectionTrigger.longPressHold:
desiredGestures
..add(GestureType.onTap)
..add(GestureType.onLongPress)
..add(GestureType.onDrag);
break;
default:
throw new ArgumentError(
'Slider does not support the event trigger ' + '"${eventTrigger}"');
break;
}
return desiredGestures;
}
@override
common.Slider<D> createCommonBehavior<D>() => new common.Slider<D>(
eventTrigger: eventTrigger,
handleRenderer: handleRenderer,
initialDomainValue: initialDomainValue as D,
onChangeCallback: onChangeCallback,
roleId: roleId,
snapToDatum: snapToDatum,
style: style);
@override
void updateCommonBehavior(common.Slider commonBehavior) {}
@override
String get role => 'Slider-${eventTrigger.toString()}';
@override
bool operator ==(Object o) {
return o is Slider &&
eventTrigger == o.eventTrigger &&
handleRenderer == o.handleRenderer &&
initialDomainValue == o.initialDomainValue &&
onChangeCallback == o.onChangeCallback &&
roleId == o.roleId &&
snapToDatum == o.snapToDatum &&
style == o.style &&
layoutPaintOrder == o.layoutPaintOrder;
}
@override
int get hashCode {
return hashValues(eventTrigger, handleRenderer, initialDomainValue, roleId,
snapToDatum, style, layoutPaintOrder);
}
}

View File

@@ -0,0 +1,53 @@
// 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/common.dart' as common
show SelectionModelType, SlidingViewport;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// 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].
@immutable
class SlidingViewport extends ChartBehavior<common.SlidingViewport> {
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
SlidingViewport([this.selectionModelType = common.SelectionModelType.info]);
@override
common.SlidingViewport<D> createCommonBehavior<D>() =>
new common.SlidingViewport<D>(selectionModelType);
@override
void updateCommonBehavior(common.SlidingViewport commonBehavior) {}
@override
String get role => 'slidingViewport-${selectionModelType.toString()}';
@override
bool operator ==(Object o) =>
o is SlidingViewport && selectionModelType == o.selectionModelType;
@override
int get hashCode => selectionModelType.hashCode;
}

View File

@@ -0,0 +1,131 @@
// 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:flutter_web/widgets.dart' show AnimationController;
import 'package:charts_common/common.dart' as common
show BaseChart, ChartBehavior, InitialHintBehavior;
import 'package:meta/meta.dart' show immutable;
import '../../base_chart_state.dart' show BaseChartState;
import '../chart_behavior.dart'
show ChartBehavior, ChartStateBehavior, GestureType;
@immutable
class InitialHintBehavior extends ChartBehavior<common.InitialHintBehavior> {
final desiredGestures = new Set<GestureType>();
final Duration hintDuration;
final double maxHintTranslate;
final double maxHintScaleFactor;
InitialHintBehavior(
{this.hintDuration, this.maxHintTranslate, this.maxHintScaleFactor});
@override
common.InitialHintBehavior<D> createCommonBehavior<D>() {
final behavior = new FlutterInitialHintBehavior<D>();
if (hintDuration != null) {
behavior.hintDuration = hintDuration;
}
if (maxHintTranslate != null) {
behavior.maxHintTranslate = maxHintTranslate;
}
if (maxHintScaleFactor != null) {
behavior.maxHintScaleFactor = maxHintScaleFactor;
}
return behavior;
}
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {}
@override
String get role => 'InitialHint';
bool operator ==(Object other) {
return other is InitialHintBehavior && other.hintDuration == hintDuration;
}
int get hashCode {
return hintDuration.hashCode;
}
}
/// Adds a native animation controller required for [common.InitialHintBehavior]
/// to function.
class FlutterInitialHintBehavior<D> extends common.InitialHintBehavior<D>
implements ChartStateBehavior {
AnimationController _hintAnimator;
BaseChartState _chartState;
set chartState(BaseChartState chartState) {
assert(chartState != null);
_chartState = chartState;
_hintAnimator = _chartState.getAnimationController(this);
_hintAnimator?.addListener(onHintTick);
}
@override
void startHintAnimation() {
super.startHintAnimation();
_hintAnimator
..duration = hintDuration
..forward(from: 0.0);
}
@override
void stopHintAnimation() {
super.stopHintAnimation();
_hintAnimator?.stop();
// Hint animation occurs only on the first draw. The hint animator is no
// longer needed after the hint animation stops and is removed.
_chartState.disposeAnimationController(this);
_hintAnimator = null;
}
@override
double get hintAnimationPercent => _hintAnimator.value;
bool _skippedFirstTick = true;
@override
void onHintTick() {
// Skip the first tick on Flutter because the widget rebuild scheduled
// during onAnimation fails on an assert on render object in the framework.
if (_skippedFirstTick) {
_skippedFirstTick = false;
return;
}
super.onHintTick();
}
@override
removeFrom(common.BaseChart<D> chart) {
_chartState.disposeAnimationController(this);
_hintAnimator = null;
super.removeFrom(chart);
}
}

View File

@@ -0,0 +1,64 @@
// 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/common.dart' as common
show ChartBehavior, PanAndZoomBehavior, PanningCompletedCallback;
import 'package:meta/meta.dart' show immutable;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
import 'pan_behavior.dart' show FlutterPanBehaviorMixin;
@immutable
class PanAndZoomBehavior extends ChartBehavior<common.PanAndZoomBehavior> {
final _desiredGestures = new Set<GestureType>.from([
GestureType.onDrag,
]);
Set<GestureType> get desiredGestures => _desiredGestures;
/// Optional callback that is called when pan / zoom is completed.
///
/// When flinging this callback is called after the fling is completed.
/// This is because panning is only completed when the flinging stops.
final common.PanningCompletedCallback panningCompletedCallback;
PanAndZoomBehavior({this.panningCompletedCallback});
@override
common.PanAndZoomBehavior<D> createCommonBehavior<D>() {
return new FlutterPanAndZoomBehavior<D>()
..panningCompletedCallback = panningCompletedCallback;
}
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {}
@override
String get role => 'PanAndZoom';
bool operator ==(Object other) {
return other is PanAndZoomBehavior &&
other.panningCompletedCallback == panningCompletedCallback;
}
int get hashCode {
return panningCompletedCallback.hashCode;
}
}
/// Adds fling gesture support to [common.PanAndZoomBehavior], by way of
/// [FlutterPanBehaviorMixin].
class FlutterPanAndZoomBehavior<D> extends common.PanAndZoomBehavior<D>
with FlutterPanBehaviorMixin {}

View File

@@ -0,0 +1,186 @@
// 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, pow, Point;
import 'package:flutter_web_ui/ui.dart' hide Point;
import 'package:flutter_web/widgets.dart' show AnimationController;
import 'package:charts_common/common.dart' as common
show BaseChart, ChartBehavior, PanBehavior, PanningCompletedCallback;
import 'package:meta/meta.dart' show immutable;
import '../../base_chart_state.dart' show BaseChartState;
import '../chart_behavior.dart'
show ChartBehavior, ChartStateBehavior, GestureType;
@immutable
class PanBehavior extends ChartBehavior<common.PanBehavior> {
final _desiredGestures = new Set<GestureType>.from([
GestureType.onDrag,
]);
/// Optional callback that is called when panning is completed.
///
/// When flinging this callback is called after the fling is completed.
/// This is because panning is only completed when the flinging stops.
final common.PanningCompletedCallback panningCompletedCallback;
PanBehavior({this.panningCompletedCallback});
Set<GestureType> get desiredGestures => _desiredGestures;
@override
common.PanBehavior<D> createCommonBehavior<D>() {
return new FlutterPanBehavior<D>()
..panningCompletedCallback = panningCompletedCallback;
}
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {}
@override
String get role => 'Pan';
bool operator ==(Object other) {
return other is PanBehavior &&
other.panningCompletedCallback == panningCompletedCallback;
}
int get hashCode {
return panningCompletedCallback.hashCode;
}
}
/// Class extending [common.PanBehavior] with fling gesture support.
class FlutterPanBehavior<D> = common.PanBehavior<D>
with FlutterPanBehaviorMixin;
/// Mixin that adds fling gesture support to [common.PanBehavior] or subclasses
/// thereof.
mixin FlutterPanBehaviorMixin<D> on common.PanBehavior<D>
implements ChartStateBehavior {
BaseChartState _chartState;
set chartState(BaseChartState chartState) {
assert(chartState != null);
_chartState = chartState;
_flingAnimator = _chartState.getAnimationController(this);
_flingAnimator?.addListener(_onFlingTick);
}
AnimationController _flingAnimator;
double _flingAnimationInitialTranslatePx;
double _flingAnimationTargetTranslatePx;
bool _isFlinging = false;
static const flingDistanceMultiplier = 0.15;
static const flingDeceleratorFactor = 1.0;
static const flingDurationMultiplier = 0.15;
static const minimumFlingVelocity = 300.0;
@override
removeFrom(common.BaseChart<D> chart) {
stopFlingAnimation();
_chartState.disposeAnimationController(this);
_flingAnimator = null;
super.removeFrom(chart);
}
@override
bool onTapTest(Point<double> chartPoint) {
super.onTapTest(chartPoint);
stopFlingAnimation();
return true;
}
@override
bool onDragEnd(
Point<double> localPosition, double scale, double pixelsPerSec) {
if (isPanning) {
// Ignore slow drag gestures to avoid jitter.
if (pixelsPerSec.abs() < minimumFlingVelocity) {
onPanEnd();
return true;
}
_startFling(pixelsPerSec);
}
return super.onDragEnd(localPosition, scale, pixelsPerSec);
}
/// Starts a 'fling' in the direction and speed given by [pixelsPerSec].
void _startFling(double pixelsPerSec) {
final domainAxis = chart.domainAxis;
_flingAnimationInitialTranslatePx = domainAxis.viewportTranslatePx;
_flingAnimationTargetTranslatePx = _flingAnimationInitialTranslatePx +
pixelsPerSec * flingDistanceMultiplier;
final flingDuration = new Duration(
milliseconds:
max(200, (pixelsPerSec * flingDurationMultiplier).abs().round()));
_flingAnimator
..duration = flingDuration
..forward(from: 0.0);
_isFlinging = true;
}
/// Decelerates a fling event.
double _decelerate(double value) => flingDeceleratorFactor == 1.0
? 1.0 - (1.0 - value) * (1.0 - value)
: 1.0 - pow(1.0 - value, 2 * flingDeceleratorFactor);
/// Updates the chart axis state on each tick of the [AnimationController].
void _onFlingTick() {
if (!_isFlinging) {
return;
}
final percent = _flingAnimator.value;
final deceleratedPercent = _decelerate(percent);
final translation = lerpDouble(_flingAnimationInitialTranslatePx,
_flingAnimationTargetTranslatePx, deceleratedPercent);
final domainAxis = chart.domainAxis;
domainAxis.setViewportSettings(
domainAxis.viewportScalingFactor, translation,
drawAreaWidth: chart.drawAreaBounds.width);
if (percent >= 1.0) {
stopFlingAnimation();
onPanEnd();
chart.redraw();
} else {
chart.redraw(skipAnimation: true, skipLayout: true);
}
}
/// Stops any current fling animations that may be executing.
void stopFlingAnimation() {
if (_isFlinging) {
_isFlinging = false;
_flingAnimator?.stop();
}
}
}

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 'dart:math' show cos, pi, sin, Point;
import 'package:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show Color;
/// Draws a sector of a circle, with an optional hole in the center.
class CircleSectorPainter {
/// Draws 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 draw(
{Canvas canvas,
Paint paint,
Point center,
double radius,
double innerRadius,
double startAngle,
double endAngle,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b);
paint.style = PaintingStyle.fill;
final innerRadiusStartPoint = new Point<double>(
innerRadius * cos(startAngle) + center.x,
innerRadius * sin(startAngle) + center.y);
final innerRadiusEndPoint = new Point<double>(
innerRadius * cos(endAngle) + center.x,
innerRadius * sin(endAngle) + center.y);
final radiusStartPoint = new Point<double>(
radius * cos(startAngle) + center.x,
radius * sin(startAngle) + center.y);
final centerOffset = new Offset(center.x, center.y);
final isFullCircle = startAngle != null &&
endAngle != null &&
endAngle - startAngle == 2 * pi;
final midpointAngle = (endAngle + startAngle) / 2;
final path = new Path()
..moveTo(innerRadiusStartPoint.x, innerRadiusStartPoint.y);
path.lineTo(radiusStartPoint.x, radiusStartPoint.y);
// For full circles, draw the arc in two parts.
if (isFullCircle) {
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius),
startAngle, midpointAngle - startAngle, true);
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius),
midpointAngle, endAngle - midpointAngle, true);
} else {
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius),
startAngle, endAngle - startAngle, true);
}
path.lineTo(innerRadiusEndPoint.x, innerRadiusEndPoint.y);
// For full circles, draw the arc in two parts.
if (isFullCircle) {
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius),
endAngle, midpointAngle - endAngle, true);
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius),
midpointAngle, startAngle - midpointAngle, true);
} else {
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius),
endAngle, startAngle - endAngle, true);
}
// Drawing two copies of this line segment, before and after the arcs,
// ensures that the path actually gets closed correctly.
path.lineTo(radiusStartPoint.x, radiusStartPoint.y);
canvas.drawPath(path, paint);
}
}

View File

@@ -0,0 +1,242 @@
// 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:flutter_web_ui/ui.dart' as ui show Shader;
import 'dart:math' show Point, Rectangle;
import 'package:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show Color;
/// Draws a simple line.
///
/// Lines may be styled with dash patterns similar to stroke-dasharray in SVG
/// path elements. Dash patterns are currently only supported between vertical
/// or horizontal line segments at this time.
class LinePainter {
/// Draws 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 draw(
{Canvas canvas,
Paint paint,
List<Point> points,
Rectangle<num> clipBounds,
common.Color fill,
common.Color stroke,
bool roundEndCaps,
double strokeWidthPx,
List<int> dashPattern,
ui.Shader shader}) {
if (points.isEmpty) {
return;
}
// Apply clip bounds as a clip region.
if (clipBounds != null) {
canvas
..save()
..clipRect(new Rect.fromLTWH(
clipBounds.left.toDouble(),
clipBounds.top.toDouble(),
clipBounds.width.toDouble(),
clipBounds.height.toDouble()));
}
paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b);
if (shader != null) {
paint.shader = shader;
}
// If the line has a single point, draw a circle.
if (points.length == 1) {
final point = points.first;
paint.style = PaintingStyle.fill;
canvas.drawCircle(new Offset(point.x, point.y), strokeWidthPx, paint);
} else {
if (strokeWidthPx != null) {
paint.strokeWidth = strokeWidthPx;
}
paint.strokeJoin = StrokeJoin.round;
paint.style = PaintingStyle.stroke;
if (dashPattern == null || dashPattern.isEmpty) {
if (roundEndCaps == true) {
paint.strokeCap = StrokeCap.round;
}
_drawSolidLine(canvas, paint, points);
} else {
_drawDashedLine(canvas, paint, points, dashPattern);
}
}
if (clipBounds != null) {
canvas.restore();
}
}
/// Draws solid lines between each point.
void _drawSolidLine(Canvas canvas, Paint paint, List<Point> points) {
// TODO: Extract a native line component which constructs the
// appropriate underlying data structures to avoid conversion.
final path = new Path()
..moveTo(points.first.x.toDouble(), points.first.y.toDouble());
for (var point in points) {
path.lineTo(point.x.toDouble(), point.y.toDouble());
}
canvas.drawPath(path, paint);
}
/// Draws dashed lines lines between each point.
void _drawDashedLine(
Canvas canvas, Paint paint, List<Point> points, List<int> dashPattern) {
final localDashPattern = new List.from(dashPattern);
// If an odd number of parts are defined, repeat the pattern to get an even
// number.
if (dashPattern.length % 2 == 1) {
localDashPattern.addAll(dashPattern);
}
// Stores the previous point in the series.
var previousSeriesPoint = _getOffset(points.first);
var remainder = 0;
var solid = true;
var dashPatternIndex = 0;
// Gets the next segment in the dash pattern, looping back to the
// beginning once the end has been reached.
var getNextDashPatternSegment = () {
final dashSegment = localDashPattern[dashPatternIndex];
dashPatternIndex = (dashPatternIndex + 1) % localDashPattern.length;
return dashSegment;
};
// Array of points that is used to draw a connecting path when only a
// partial dash pattern segment can be drawn in the remaining length of a
// line segment (between two defined points in the shape).
var remainderPoints;
// Draw the path through all the rest of the points in the series.
for (var pointIndex = 1; pointIndex < points.length; pointIndex++) {
// Stores the current point in the series.
final seriesPoint = _getOffset(points[pointIndex]);
if (previousSeriesPoint == seriesPoint) {
// Bypass dash pattern handling if the points are the same.
} else {
// Stores the previous point along the current series line segment where
// we rendered a dash (or left a gap).
var previousPoint = previousSeriesPoint;
var d = _getOffsetDistance(previousSeriesPoint, seriesPoint);
while (d > 0) {
var dashSegment =
remainder > 0 ? remainder : getNextDashPatternSegment();
remainder = 0;
// Create a unit vector in the direction from previous to next point.
final v = seriesPoint - previousPoint;
final u = new Offset(v.dx / v.distance, v.dy / v.distance);
// If the remaining distance is less than the length of the dash
// pattern segment, then cut off the pattern segment for this portion
// of the overall line.
final distance = d < dashSegment ? d : dashSegment.toDouble();
// Compute a vector representing the length of dash pattern segment to
// be drawn.
final nextPoint = previousPoint + (u * distance);
// If we are in a solid portion of the dash pattern, draw a line.
// Else, move on.
if (solid) {
if (remainderPoints != null) {
// If we had a partial un-drawn dash from the previous point along
// the line, draw a path that includes it and the end of the dash
// pattern segment in the current line segment.
remainderPoints.add(new Offset(nextPoint.dx, nextPoint.dy));
final path = new Path()
..moveTo(remainderPoints.first.dx, remainderPoints.first.dy);
for (var p in remainderPoints) {
path.lineTo(p.dx, p.dy);
}
canvas.drawPath(path, paint);
remainderPoints = null;
} else {
if (d < dashSegment && pointIndex < points.length - 1) {
// If the remaining distance d is too small to fit this dash,
// and we have more points in the line, save off a series of
// remainder points so that we can draw a path segment moving in
// the direction of the next point.
//
// Note that we don't need to save anything off for the "blank"
// portions of the pattern because we still take the remaining
// distance into account before starting the next dash in the
// next line segment.
remainderPoints = [
new Offset(previousPoint.dx, previousPoint.dy),
new Offset(nextPoint.dx, nextPoint.dy)
];
} else {
// Otherwise, draw a simple line segment for this dash.
canvas.drawLine(previousPoint, nextPoint, paint);
}
}
}
solid = !solid;
previousPoint = nextPoint;
d = d - dashSegment;
}
// Save off the remaining distance so that we can continue the dash (or
// gap) into the next line segment.
remainder = -d.round();
// If we have a remaining un-drawn distance for the current dash (or
// gap), revert the last change to "solid" so that we will continue
// either drawing a dash or leaving a gap.
if (remainder > 0) {
solid = !solid;
}
}
previousSeriesPoint = seriesPoint;
}
}
/// Converts a [Point] into an [Offset].
Offset _getOffset(Point point) =>
new Offset(point.x.toDouble(), point.y.toDouble());
/// Computes the distance between two [Offset]s, as if they were [Point]s.
num _getOffsetDistance(Offset o1, Offset o2) {
final p1 = new Point(o1.dx, o1.dy);
final p2 = new Point(o2.dx, o2.dy);
return p1.distanceTo(p2);
}
}

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 'dart:math' show cos, sin, Point;
import 'package:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show CanvasPie;
import 'circle_sector_painter.dart' show CircleSectorPainter;
/// Draws a pie chart, with an optional hole in the center.
class PiePainter {
CircleSectorPainter _circleSectorPainter;
/// Draws a pie chart, with an optional hole in the center.
void draw(Canvas canvas, Paint paint, common.CanvasPie canvasPie) {
_circleSectorPainter ??= new CircleSectorPainter();
final center = canvasPie.center;
final radius = canvasPie.radius;
final innerRadius = canvasPie.innerRadius;
for (var slice in canvasPie.slices) {
_circleSectorPainter.draw(
canvas: canvas,
paint: paint,
center: center,
radius: radius,
innerRadius: innerRadius,
startAngle: slice.startAngle,
endAngle: slice.endAngle,
fill: slice.fill);
}
// Draw stroke lines between pie slices. This is done after the slices are
// drawn to ensure that they appear on top.
if (canvasPie.stroke != null &&
canvasPie.strokeWidthPx != null &&
canvasPie.slices.length > 1) {
paint.color = new Color.fromARGB(canvasPie.stroke.a, canvasPie.stroke.r,
canvasPie.stroke.g, canvasPie.stroke.b);
paint.strokeWidth = canvasPie.strokeWidthPx;
paint.strokeJoin = StrokeJoin.bevel;
paint.style = PaintingStyle.stroke;
final path = new Path();
for (var slice in canvasPie.slices) {
final innerRadiusStartPoint = new Point<double>(
innerRadius * cos(slice.startAngle) + center.x,
innerRadius * sin(slice.startAngle) + center.y);
final innerRadiusEndPoint = new Point<double>(
innerRadius * cos(slice.endAngle) + center.x,
innerRadius * sin(slice.endAngle) + center.y);
final radiusStartPoint = new Point<double>(
radius * cos(slice.startAngle) + center.x,
radius * sin(slice.startAngle) + center.y);
final radiusEndPoint = new Point<double>(
radius * cos(slice.endAngle) + center.x,
radius * sin(slice.endAngle) + center.y);
path.moveTo(innerRadiusStartPoint.x, innerRadiusStartPoint.y);
path.lineTo(radiusStartPoint.x, radiusStartPoint.y);
path.moveTo(innerRadiusEndPoint.x, innerRadiusEndPoint.y);
path.lineTo(radiusEndPoint.x, radiusEndPoint.y);
}
canvas.drawPath(path, paint);
}
}
}

View File

@@ -0,0 +1,56 @@
// 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:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show Color;
/// Draws a simple point.
///
/// TODO: Support for more shapes than circles?
class PointPainter {
void draw(
{Canvas canvas,
Paint paint,
Point point,
double radius,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
if (point == null) {
return;
}
if (fill != null) {
paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b);
paint.style = PaintingStyle.fill;
canvas.drawCircle(
new Offset(point.x.toDouble(), point.y.toDouble()), radius, paint);
}
// [Canvas.drawCircle] does not support drawing a circle with both a fill
// and a stroke at this time. Use a separate circle for the stroke.
if (stroke != null && strokeWidthPx != null && strokeWidthPx > 0.0) {
paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b);
paint.strokeWidth = strokeWidthPx;
paint.strokeJoin = StrokeJoin.bevel;
paint.style = PaintingStyle.stroke;
canvas.drawCircle(
new Offset(point.x.toDouble(), point.y.toDouble()), radius, paint);
}
}
}

View File

@@ -0,0 +1,96 @@
// 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 'package:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show Color;
/// Draws a simple line.
///
/// Lines may be styled with dash patterns similar to stroke-dasharray in SVG
/// path elements. Dash patterns are currently only supported between vertical
/// or horizontal line segments at this time.
class PolygonPainter {
/// Draws 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 draw(
{Canvas canvas,
Paint paint,
List<Point> points,
Rectangle<num> clipBounds,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
if (points.isEmpty) {
return;
}
// Apply clip bounds as a clip region.
if (clipBounds != null) {
canvas
..save()
..clipRect(new Rect.fromLTWH(
clipBounds.left.toDouble(),
clipBounds.top.toDouble(),
clipBounds.width.toDouble(),
clipBounds.height.toDouble()));
}
final strokeColor = stroke != null
? new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b)
: null;
final fillColor = fill != null
? new Color.fromARGB(fill.a, fill.r, fill.g, fill.b)
: null;
// If the line has a single point, draw a circle.
if (points.length == 1) {
final point = points.first;
paint.color = fillColor;
paint.style = PaintingStyle.fill;
canvas.drawCircle(new Offset(point.x, point.y), strokeWidthPx, paint);
} else {
if (strokeColor != null && strokeWidthPx != null) {
paint.strokeWidth = strokeWidthPx;
paint.strokeJoin = StrokeJoin.bevel;
paint.style = PaintingStyle.stroke;
}
if (fillColor != null) {
paint.color = fillColor;
paint.style = PaintingStyle.fill;
}
final path = new Path()
..moveTo(points.first.x.toDouble(), points.first.y.toDouble());
for (var point in points) {
path.lineTo(point.x.toDouble(), point.y.toDouble());
}
canvas.drawPath(path, paint);
}
if (clipBounds != null) {
canvas.restore();
}
}
}

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:collection' show LinkedHashMap;
import 'package:meta/meta.dart' show immutable, protected;
import 'package:charts_common/common.dart' as common
show
AxisSpec,
BaseChart,
CartesianChart,
NumericAxis,
NumericAxisSpec,
RTLSpec,
Series,
SeriesRendererConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'base_chart.dart' show BaseChart, LayoutConfig;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
@immutable
abstract class CartesianChart<D> extends BaseChart<D> {
final common.AxisSpec domainAxis;
final common.AxisSpec primaryMeasureAxis;
final common.AxisSpec secondaryMeasureAxis;
final LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes;
final bool flipVerticalAxis;
CartesianChart(
List<common.Series<dynamic, D>> seriesList, {
bool animate,
Duration animationDuration,
this.domainAxis,
this.primaryMeasureAxis,
this.secondaryMeasureAxis,
this.disjointMeasureAxes,
common.SeriesRendererConfig<D> defaultRenderer,
List<common.SeriesRendererConfig<D>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<D>> selectionModels,
common.RTLSpec rtlSpec,
bool defaultInteractions: true,
LayoutConfig layoutConfig,
UserManagedState userManagedState,
this.flipVerticalAxis,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
defaultInteractions: defaultInteractions,
layoutConfig: layoutConfig,
userManagedState: userManagedState,
);
@override
void updateCommonChart(common.BaseChart baseChart, BaseChart oldWidget,
BaseChartState chartState) {
super.updateCommonChart(baseChart, oldWidget, chartState);
final prev = oldWidget as CartesianChart;
final chart = baseChart as common.CartesianChart;
if (flipVerticalAxis != null) {
chart.flipVerticalAxisOutput = flipVerticalAxis;
}
if (domainAxis != null && domainAxis != prev?.domainAxis) {
chart.domainAxisSpec = domainAxis;
chartState.markChartDirty();
}
if (primaryMeasureAxis != null &&
primaryMeasureAxis != prev?.primaryMeasureAxis) {
chart.primaryMeasureAxisSpec = primaryMeasureAxis;
chartState.markChartDirty();
}
if (secondaryMeasureAxis != null &&
secondaryMeasureAxis != prev?.secondaryMeasureAxis) {
chart.secondaryMeasureAxisSpec = secondaryMeasureAxis;
chartState.markChartDirty();
}
if (disjointMeasureAxes != null &&
disjointMeasureAxes != prev?.disjointMeasureAxes) {
chart.disjointMeasureAxisSpecs = disjointMeasureAxes;
chartState.markChartDirty();
}
}
@protected
LinkedHashMap<String, common.NumericAxis> createDisjointMeasureAxes() {
if (disjointMeasureAxes != null) {
final disjointAxes = new LinkedHashMap<String, common.NumericAxis>();
disjointMeasureAxes
.forEach((String axisId, common.NumericAxisSpec axisSpec) {
disjointAxes[axisId] = axisSpec.createAxis();
});
return disjointAxes;
} else {
return null;
}
}
}

View File

@@ -0,0 +1,442 @@
// 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:flutter_web_ui/ui.dart' as ui show Gradient, Shader;
import 'dart:math' show Point, Rectangle, max;
import 'package:charts_common/common.dart' as common
show
ChartCanvas,
CanvasBarStack,
CanvasPie,
Color,
FillPatternType,
GraphicsFactory,
StyleFactory,
TextElement,
TextDirection;
import 'package:flutter_web/material.dart';
import 'text_element.dart' show TextElement;
import 'canvas/circle_sector_painter.dart' show CircleSectorPainter;
import 'canvas/line_painter.dart' show LinePainter;
import 'canvas/pie_painter.dart' show PiePainter;
import 'canvas/point_painter.dart' show PointPainter;
import 'canvas/polygon_painter.dart' show PolygonPainter;
class ChartCanvas implements common.ChartCanvas {
/// Pixels to allow to overdraw above the draw area that fades to transparent.
static const double rect_top_gradient_pixels = 5;
final Canvas canvas;
final common.GraphicsFactory graphicsFactory;
final _paint = new Paint();
CircleSectorPainter _circleSectorPainter;
LinePainter _linePainter;
PiePainter _piePainter;
PointPainter _pointPainter;
PolygonPainter _polygonPainter;
ChartCanvas(this.canvas, this.graphicsFactory);
@override
void drawCircleSector(Point center, double radius, double innerRadius,
double startAngle, double endAngle,
{common.Color fill, common.Color stroke, double strokeWidthPx}) {
_circleSectorPainter ??= new CircleSectorPainter();
_circleSectorPainter.draw(
canvas: canvas,
paint: _paint,
center: center,
radius: radius,
innerRadius: innerRadius,
startAngle: startAngle,
endAngle: endAngle,
fill: fill,
stroke: stroke,
strokeWidthPx: strokeWidthPx);
}
@override
void drawLine(
{List<Point> points,
Rectangle<num> clipBounds,
common.Color fill,
common.Color stroke,
bool roundEndCaps,
double strokeWidthPx,
List<int> dashPattern}) {
_linePainter ??= new LinePainter();
_linePainter.draw(
canvas: canvas,
paint: _paint,
points: points,
clipBounds: clipBounds,
fill: fill,
stroke: stroke,
roundEndCaps: roundEndCaps,
strokeWidthPx: strokeWidthPx,
dashPattern: dashPattern);
}
@override
void drawPie(common.CanvasPie canvasPie) {
_piePainter ??= new PiePainter();
_piePainter.draw(canvas, _paint, canvasPie);
}
@override
void drawPoint(
{Point point,
double radius,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
_pointPainter ??= new PointPainter();
_pointPainter.draw(
canvas: canvas,
paint: _paint,
point: point,
radius: radius,
fill: fill,
stroke: stroke,
strokeWidthPx: strokeWidthPx);
}
@override
void drawPolygon(
{List<Point> points,
Rectangle<num> clipBounds,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
_polygonPainter ??= new PolygonPainter();
_polygonPainter.draw(
canvas: canvas,
paint: _paint,
points: points,
clipBounds: clipBounds,
fill: fill,
stroke: stroke,
strokeWidthPx: strokeWidthPx);
}
/// Creates a bottom to top gradient that transitions [fill] to transparent.
ui.Gradient _createHintGradient(double left, double top, common.Color fill) {
return new ui.Gradient.linear(
new Offset(left, top),
new Offset(left, top - rect_top_gradient_pixels),
[
new Color.fromARGB(fill.a, fill.r, fill.g, fill.b),
new Color.fromARGB(0, fill.r, fill.g, fill.b)
],
);
}
@override
void drawRect(Rectangle<num> bounds,
{common.Color fill,
common.FillPatternType pattern,
common.Color stroke,
double strokeWidthPx,
Rectangle<num> drawAreaBounds}) {
final drawStroke =
(strokeWidthPx != null && strokeWidthPx > 0.0 && stroke != null);
final strokeWidthOffset = (drawStroke ? strokeWidthPx : 0);
// Factor out stroke width, if a stroke is enabled.
final fillRectBounds = new Rectangle<num>(
bounds.left + strokeWidthOffset / 2,
bounds.top + strokeWidthOffset / 2,
bounds.width - strokeWidthOffset,
bounds.height - strokeWidthOffset);
switch (pattern) {
case common.FillPatternType.forwardHatch:
_drawForwardHatchPattern(fillRectBounds, canvas,
fill: fill, drawAreaBounds: drawAreaBounds);
break;
case common.FillPatternType.solid:
default:
// Use separate rect for drawing stroke
_paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b);
_paint.style = PaintingStyle.fill;
// Apply a gradient to the top [rect_top_gradient_pixels] to transparent
// if the rectangle is higher than the [drawAreaBounds] top.
if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) {
_paint.shader = _createHintGradient(drawAreaBounds.left.toDouble(),
drawAreaBounds.top.toDouble(), fill);
}
canvas.drawRect(_getRect(fillRectBounds), _paint);
break;
}
// [Canvas.drawRect] does not support drawing a rectangle with both a fill
// and a stroke at this time. Use a separate rect for the stroke.
if (drawStroke) {
_paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b);
// Set shader to null if no draw area bounds so it can use the color
// instead.
_paint.shader = drawAreaBounds != null
? _createHintGradient(drawAreaBounds.left.toDouble(),
drawAreaBounds.top.toDouble(), stroke)
: null;
_paint.strokeJoin = StrokeJoin.round;
_paint.strokeWidth = strokeWidthPx;
_paint.style = PaintingStyle.stroke;
canvas.drawRect(_getRect(bounds), _paint);
}
// Reset the shader.
_paint.shader = null;
}
@override
void drawRRect(Rectangle<num> bounds,
{common.Color fill,
common.Color stroke,
num radius,
bool roundTopLeft,
bool roundTopRight,
bool roundBottomLeft,
bool roundBottomRight}) {
// Use separate rect for drawing stroke
_paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b);
_paint.style = PaintingStyle.fill;
canvas.drawRRect(
_getRRect(bounds,
radius: radius,
roundTopLeft: roundTopLeft,
roundTopRight: roundTopRight,
roundBottomLeft: roundBottomLeft,
roundBottomRight: roundBottomRight),
_paint);
}
@override
void drawBarStack(common.CanvasBarStack barStack,
{Rectangle<num> drawAreaBounds}) {
// only clip if rounded rect.
// Clip a rounded rect for the whole region if rounded bars.
final roundedCorners = 0 < barStack.radius;
if (roundedCorners) {
canvas
..save()
..clipRRect(_getRRect(
barStack.fullStackRect,
radius: barStack.radius.toDouble(),
roundTopLeft: barStack.roundTopLeft,
roundTopRight: barStack.roundTopRight,
roundBottomLeft: barStack.roundBottomLeft,
roundBottomRight: barStack.roundBottomRight,
));
}
// Draw each bar.
for (var barIndex = 0; barIndex < barStack.segments.length; barIndex++) {
// TODO: Add configuration for hiding stack line.
// TODO: Don't draw stroke on bottom of bars.
final segment = barStack.segments[barIndex];
drawRect(segment.bounds,
fill: segment.fill,
pattern: segment.pattern,
stroke: segment.stroke,
strokeWidthPx: segment.strokeWidthPx,
drawAreaBounds: drawAreaBounds);
}
if (roundedCorners) {
canvas.restore();
}
}
@override
void drawText(common.TextElement textElement, int offsetX, int offsetY,
{double rotation = 0.0}) {
// Must be Flutter TextElement.
assert(textElement is TextElement);
final flutterTextElement = textElement as TextElement;
final textDirection = flutterTextElement.textDirection;
final measurement = flutterTextElement.measurement;
if (rotation != 0) {
// TODO: Remove once textAnchor works.
if (textDirection == common.TextDirection.rtl) {
offsetY += measurement.horizontalSliceWidth.toInt();
}
offsetX -= flutterTextElement.verticalFontShift;
canvas.save();
canvas.translate(offsetX.toDouble(), offsetY.toDouble());
canvas.rotate(rotation);
(textElement as TextElement)
.textPainter
.paint(canvas, new Offset(0.0, 0.0));
canvas.restore();
} else {
// TODO: Remove once textAnchor works.
if (textDirection == common.TextDirection.rtl) {
offsetX -= measurement.horizontalSliceWidth.toInt();
}
// Account for missing center alignment.
if (textDirection == common.TextDirection.center) {
offsetX -= (measurement.horizontalSliceWidth / 2).ceil();
}
offsetY -= flutterTextElement.verticalFontShift;
(textElement as TextElement)
.textPainter
.paint(canvas, new Offset(offsetX.toDouble(), offsetY.toDouble()));
}
}
@override
void setClipBounds(Rectangle<int> clipBounds) {
canvas
..save()
..clipRect(_getRect(clipBounds));
}
@override
void resetClipBounds() {
canvas.restore();
}
/// Convert dart:math [Rectangle] to Flutter [Rect].
Rect _getRect(Rectangle<num> rectangle) {
return new Rect.fromLTWH(
rectangle.left.toDouble(),
rectangle.top.toDouble(),
rectangle.width.toDouble(),
rectangle.height.toDouble());
}
/// Convert dart:math [Rectangle] and to Flutter [RRect].
RRect _getRRect(
Rectangle<num> rectangle, {
double radius,
bool roundTopLeft = false,
bool roundTopRight = false,
bool roundBottomLeft = false,
bool roundBottomRight = false,
}) {
final cornerRadius =
radius == 0 ? Radius.zero : new Radius.circular(radius);
return new RRect.fromLTRBAndCorners(
rectangle.left.toDouble(),
rectangle.top.toDouble(),
rectangle.right.toDouble(),
rectangle.bottom.toDouble(),
topLeft: roundTopLeft ? cornerRadius : Radius.zero,
topRight: roundTopRight ? cornerRadius : Radius.zero,
bottomLeft: roundBottomLeft ? cornerRadius : Radius.zero,
bottomRight: roundBottomRight ? cornerRadius : Radius.zero);
}
/// Draws a forward hatch pattern in the given bounds.
_drawForwardHatchPattern(
Rectangle<num> bounds,
Canvas canvas, {
common.Color background,
common.Color fill,
double fillWidthPx = 4.0,
Rectangle<num> drawAreaBounds,
}) {
background ??= common.StyleFactory.style.white;
fill ??= common.StyleFactory.style.black;
// Fill in the shape with a solid background color.
_paint.color = new Color.fromARGB(
background.a, background.r, background.g, background.b);
_paint.style = PaintingStyle.fill;
// Apply a gradient the background if bounds exceed the draw area.
if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) {
_paint.shader = _createHintGradient(drawAreaBounds.left.toDouble(),
drawAreaBounds.top.toDouble(), background);
}
canvas.drawRect(_getRect(bounds), _paint);
// As a simplification, we will treat the bounds as a large square and fill
// it up with lines from the bottom-left corner to the top-right corner.
// Get the longer side of the bounds here for the size of this square.
final size = max(bounds.width, bounds.height);
final x0 = bounds.left + size + fillWidthPx;
final x1 = bounds.left - fillWidthPx;
final y0 = bounds.bottom - size - fillWidthPx;
final y1 = bounds.bottom + fillWidthPx;
final offset = 8;
final isVertical = bounds.height >= bounds.width;
_linePainter ??= new LinePainter();
// The "first" line segment will be drawn from the bottom left corner of the
// bounds, up and towards the right. Start the loop N iterations "back" to
// draw partial line segments beneath (or to the left) of this segment,
// where N is the number of offsets that fit inside the smaller dimension of
// the bounds.
final smallSide = isVertical ? bounds.width : bounds.height;
final start = -(smallSide / offset).round() * offset;
// Keep going until we reach the top or right of the bounds, depending on
// whether the rectangle is oriented vertically or horizontally.
final end = size + offset;
// Create gradient for line painter if top bounds exceeded.
ui.Shader lineShader;
if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) {
lineShader = _createHintGradient(
drawAreaBounds.left.toDouble(), drawAreaBounds.top.toDouble(), fill);
}
for (int i = start; i < end; i = i + offset) {
// For vertical bounds, we need to draw lines from top to bottom. For
// bounds, we need to draw lines from left to right.
final modifier = isVertical ? -1 * i : i;
// Draw a line segment in the bottom right corner of the pattern.
_linePainter.draw(
canvas: canvas,
paint: _paint,
points: [
new Point(x0 + modifier, y0),
new Point(x1 + modifier, y1),
],
stroke: fill,
strokeWidthPx: fillWidthPx,
shader: lineShader);
}
}
@override
set drawingView(String viewName) {}
}

View File

@@ -0,0 +1,419 @@
// 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/common.dart' as common
show
A11yNode,
AxisDirection,
BaseChart,
ChartContext,
DateTimeFactory,
LocalDateTimeFactory,
ProxyGestureListener,
RTLSpec,
SelectionModelType,
Series,
Performance;
import 'package:flutter_web/material.dart';
import 'package:flutter_web/rendering.dart';
import 'package:flutter_web/scheduler.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart' show required;
import 'chart_canvas.dart' show ChartCanvas;
import 'chart_state.dart' show ChartState;
import 'base_chart.dart' show BaseChart;
import 'graphics_factory.dart' show GraphicsFactory;
import 'time_series_chart.dart' show TimeSeriesChart;
import 'user_managed_state.dart' show UserManagedState;
/// Widget that inflates to a [CustomPaint] that implements common [ChartContext].
class ChartContainer<D> extends CustomPaint {
final BaseChart<D> chartWidget;
final BaseChart<D> oldChartWidget;
final ChartState chartState;
final double animationValue;
final bool rtl;
final common.RTLSpec rtlSpec;
final UserManagedState<D> userManagedState;
ChartContainer(
{@required this.oldChartWidget,
@required this.chartWidget,
@required this.chartState,
@required this.animationValue,
@required this.rtl,
@required this.rtlSpec,
this.userManagedState});
@override
RenderCustomPaint createRenderObject(BuildContext context) {
return new ChartContainerRenderObject<D>()..reconfigure(this, context);
}
@override
void updateRenderObject(
BuildContext context, ChartContainerRenderObject renderObject) {
renderObject.reconfigure(this, context);
}
}
/// [RenderCustomPaint] that implements common [ChartContext].
class ChartContainerRenderObject<D> extends RenderCustomPaint
implements common.ChartContext {
common.BaseChart<D> _chart;
List<common.Series<dynamic, D>> _seriesList;
ChartState _chartState;
bool _chartContainerIsRtl = false;
common.RTLSpec _rtlSpec;
common.DateTimeFactory _dateTimeFactory;
bool _exploreMode = false;
List<common.A11yNode> _a11yNodes;
final Logger _log = new Logger('charts_flutter.charts_container');
/// Keeps the last time the configuration was changed and chart draw on the
/// common chart is called.
///
/// An assert uses this value to check if the configuration changes more
/// frequently than a threshold. This is to notify developers of something
/// wrong in the configuration of their charts if it keeps changes (usually
/// due to equality checks not being implemented and when a new object is
/// created inside a new chart widget, a change is detected even if nothing
/// has changed).
DateTime _lastConfigurationChangeTime;
/// The minimum time required before the next configuration change.
static const configurationChangeThresholdMs = 500;
void reconfigure(ChartContainer config, BuildContext context) {
_chartState = config.chartState;
_dateTimeFactory = (config.chartWidget is TimeSeriesChart)
? (config.chartWidget as TimeSeriesChart).dateTimeFactory
: null;
_dateTimeFactory ??= new common.LocalDateTimeFactory();
if (_chart == null) {
common.Performance.time('chartsCreate');
_chart = config.chartWidget.createCommonChart(_chartState);
_chart.init(this, new GraphicsFactory(context));
common.Performance.timeEnd('chartsCreate');
}
common.Performance.time('chartsConfig');
config.chartWidget
.updateCommonChart(_chart, config.oldChartWidget, _chartState);
_rtlSpec = config.rtlSpec ?? const common.RTLSpec();
_chartContainerIsRtl = config.rtl ?? false;
common.Performance.timeEnd('chartsConfig');
// If the configuration is changed more frequently than the threshold,
// log the occurrence and reset the configurationChanged flag to false
// to skip calling chart draw and avoid getting into an infinite rebuild
// cycle.
//
// One common cause for the configuration changing on every chart build
// is because a behavior is detected to have changed when it has not.
// A common case is when a setting is passed to a behavior is an object
// and doesn't override the equality checks.
if (_chartState.chartIsDirty) {
final currentTime = DateTime.now();
final lastConfigurationBelowThreshold = _lastConfigurationChangeTime !=
null &&
currentTime.difference(_lastConfigurationChangeTime).inMilliseconds <
configurationChangeThresholdMs;
_lastConfigurationChangeTime = currentTime;
if (lastConfigurationBelowThreshold) {
_chartState.resetChartDirtyFlag();
_log.warning(
'Chart configuration is changing more frequent than threshold'
' of $configurationChangeThresholdMs. Check if your behavior, axis,'
' or renderer config is missing equality checks that may be causing'
' configuration to be detected as changed. ');
}
}
if (_chartState.chartIsDirty) {
_chart.configurationChanged();
}
// If series list changes or other configuration changed that triggered the
// _chartState.configurationChanged flag to be set (such as axis, behavior,
// and renderer changes). Otherwise, the chart only requests repainting and
// does not reprocess the series.
//
// Series list is considered "changed" based on the instance.
if (_seriesList != config.chartWidget.seriesList ||
_chartState.chartIsDirty) {
_chartState.resetChartDirtyFlag();
_seriesList = config.chartWidget.seriesList;
// Clear out the a11y nodes generated.
_a11yNodes = null;
common.Performance.time('chartsDraw');
_chart.draw(_seriesList);
common.Performance.timeEnd('chartsDraw');
// This is needed because when a series changes we need to reset flutter's
// animation value from 1.0 back to 0.0.
_chart.animationPercent = 0.0;
markNeedsLayout();
} else {
_chart.animationPercent = config.animationValue;
markNeedsPaint();
}
_updateUserManagedState(config.userManagedState);
// Set the painter used for calling common chart for paint.
// This painter is also used to generate semantic nodes for a11y.
_setNewPainter();
}
/// If user managed state is set, check each setting to see if it is different
/// than internal chart state and only update if different.
_updateUserManagedState(UserManagedState<D> newState) {
if (newState == null) {
return;
}
// Only override the selection model if it is different than the existing
// selection model so update listeners are not unnecessarily triggered.
for (common.SelectionModelType type in newState.selectionModels.keys) {
final model = _chart.getSelectionModel(type);
final userModel =
newState.selectionModels[type].getModel(_chart.currentSeriesList);
if (model != userModel) {
model.updateSelection(
userModel.selectedDatum, userModel.selectedSeries);
}
}
}
@override
void performLayout() {
common.Performance.time('chartsLayout');
_chart.measure(constraints.maxWidth.toInt(), constraints.maxHeight.toInt());
_chart.layout(constraints.maxWidth.toInt(), constraints.maxHeight.toInt());
common.Performance.timeEnd('chartsLayout');
size = constraints.biggest;
// Check if the gestures registered in gesture registry matches what the
// common chart is listening to.
// TODO: Still need a test for this for sanity sake.
// assert(_desiredGestures
// .difference(_chart.gestureProxy.listenedGestures)
// .isEmpty);
}
@override
void markNeedsLayout() {
super.markNeedsLayout();
if (parent != null) {
markParentNeedsLayout();
}
}
@override
bool hitTestSelf(Offset position) => true;
@override
void requestRedraw() {}
@override
void requestAnimation(Duration transition) {
void startAnimationController(_) {
_chartState.setAnimation(transition);
}
// Sometimes chart behaviors try to draw the chart outside of a Flutter draw
// cycle. Schedule a frame manually to handle these cases.
if (!SchedulerBinding.instance.hasScheduledFrame) {
SchedulerBinding.instance.scheduleFrame();
}
SchedulerBinding.instance.addPostFrameCallback(startAnimationController);
}
/// Request Flutter to rebuild the widget/container of chart.
///
/// This is different than requesting redraw and paint because those only
/// affect the chart widget. This is for requesting rebuild of the Flutter
/// widget that contains the chart widget. This is necessary for supporting
/// Flutter widgets that are layout with the chart.
///
/// Example is legends, a legend widget can be layout on top of the chart
/// widget or along the sides of the chart. Requesting a rebuild allows
/// the legend to layout and redraw itself.
void requestRebuild() {
void doRebuild(_) {
_chartState.requestRebuild();
}
// Flutter does not allow requesting rebuild during the build cycle, this
// schedules rebuild request to happen after the current build cycle.
// This is needed to request rebuild after the legend has been added in the
// post process phase of the chart, which happens during the chart widget's
// build cycle.
SchedulerBinding.instance.addPostFrameCallback(doRebuild);
}
/// When Flutter's markNeedsLayout is called, layout and paint are both
/// called. If animations are off, Flutter's paint call after layout will
/// paint the chart. If animations are on, Flutter's paint is called with the
/// initial animation value and then the animation controller is started after
/// this first build cycle.
@override
void requestPaint() {
markNeedsPaint();
}
@override
double get pixelsPerDp => 1.0;
@override
bool get chartContainerIsRtl => _chartContainerIsRtl;
@override
common.RTLSpec get rtlSpec => _rtlSpec;
@override
bool get isRtl =>
_chartContainerIsRtl &&
_rtlSpec?.axisDirection == common.AxisDirection.reversed;
@override
bool get isTappable => _chart.isTappable;
@override
common.DateTimeFactory get dateTimeFactory => _dateTimeFactory;
/// Gets the chart's gesture listener.
common.ProxyGestureListener get gestureProxy => _chart.gestureProxy;
TextDirection get textDirection =>
_chartContainerIsRtl ? TextDirection.rtl : TextDirection.ltr;
@override
void enableA11yExploreMode(List<common.A11yNode> nodes,
{String announcement}) {
_a11yNodes = nodes;
_exploreMode = true;
_setNewPainter();
requestRebuild();
if (announcement != null) {
SemanticsService.announce(announcement, textDirection);
}
}
@override
void disableA11yExploreMode({String announcement}) {
_a11yNodes = [];
_exploreMode = false;
_setNewPainter();
requestRebuild();
if (announcement != null) {
SemanticsService.announce(announcement, textDirection);
}
}
void _setNewPainter() {
painter = new ChartContainerCustomPaint(
oldPainter: painter,
chart: _chart,
exploreMode: _exploreMode,
a11yNodes: _a11yNodes,
textDirection: textDirection);
}
}
class ChartContainerCustomPaint extends CustomPainter {
final common.BaseChart chart;
final bool exploreMode;
final List<common.A11yNode> a11yNodes;
final TextDirection textDirection;
factory ChartContainerCustomPaint(
{ChartContainerCustomPaint oldPainter,
common.BaseChart chart,
bool exploreMode,
List<common.A11yNode> a11yNodes,
TextDirection textDirection}) {
if (oldPainter != null &&
oldPainter.exploreMode == exploreMode &&
oldPainter.a11yNodes == a11yNodes &&
oldPainter.textDirection == textDirection) {
return oldPainter;
} else {
return new ChartContainerCustomPaint._internal(
chart: chart,
exploreMode: exploreMode ?? false,
a11yNodes: a11yNodes ?? <common.A11yNode>[],
textDirection: textDirection ?? TextDirection.ltr);
}
}
ChartContainerCustomPaint._internal(
{this.chart, this.exploreMode, this.a11yNodes, this.textDirection});
@override
void paint(Canvas canvas, Size size) {
common.Performance.time('chartsPaint');
final chartsCanvas = new ChartCanvas(canvas, chart.graphicsFactory);
chart.paint(chartsCanvas);
common.Performance.timeEnd('chartsPaint');
}
/// Common chart requests rebuild that handle repaint requests.
@override
bool shouldRepaint(ChartContainerCustomPaint oldPainter) => false;
/// Rebuild semantics when explore mode is toggled semantic properties change.
@override
bool shouldRebuildSemantics(ChartContainerCustomPaint oldDelegate) {
return exploreMode != oldDelegate.exploreMode ||
a11yNodes != oldDelegate.a11yNodes ||
textDirection != textDirection;
}
@override
SemanticsBuilderCallback get semanticsBuilder => _buildSemantics;
List<CustomPainterSemantics> _buildSemantics(Size size) {
final nodes = <CustomPainterSemantics>[];
for (common.A11yNode node in a11yNodes) {
final rect = new Rect.fromLTWH(
node.boundingBox.left.toDouble(),
node.boundingBox.top.toDouble(),
node.boundingBox.width.toDouble(),
node.boundingBox.height.toDouble());
nodes.add(new CustomPainterSemantics(
rect: rect,
properties: new SemanticsProperties(
value: node.label,
textDirection: textDirection,
onDidGainAccessibilityFocus: node.onFocus)));
}
return nodes;
}
}

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:async' show Timer;
import 'dart:math' show Point;
import 'package:flutter_web/material.dart'
show
BuildContext,
GestureDetector,
ScaleEndDetails,
ScaleStartDetails,
ScaleUpdateDetails,
TapDownDetails,
TapUpDetails;
import 'behaviors/chart_behavior.dart' show GestureType;
import 'chart_container.dart' show ChartContainer, ChartContainerRenderObject;
import 'util.dart' show getChartContainerRenderObject;
// From https://docs.flutter.io/flutter/gestures/kLongPressTimeout-constant.html
const Duration _kLongPressTimeout = const Duration(milliseconds: 500);
class ChartGestureDetector {
bool _listeningForLongPress;
bool _isDragging = false;
Timer _longPressTimer;
Point<double> _lastTapPoint;
double _lastScale;
_ContainerResolver _containerResolver;
makeWidget(BuildContext context, ChartContainer chartContainer,
Set<GestureType> desiredGestures) {
_containerResolver =
() => getChartContainerRenderObject(context.findRenderObject());
final wantTapDown = desiredGestures.isNotEmpty;
final wantTap = desiredGestures.contains(GestureType.onTap);
final wantDrag = desiredGestures.contains(GestureType.onDrag);
// LongPress is special, we'd like to be able to trigger long press before
// Drag/Press to trigger tooltips then explore with them. This means we
// can't rely on gesture detection since it will block out the scale
// gestures.
_listeningForLongPress = desiredGestures.contains(GestureType.onLongPress);
return new GestureDetector(
child: chartContainer,
onTapDown: wantTapDown ? onTapDown : null,
onTapUp: wantTap ? onTapUp : null,
onScaleStart: wantDrag ? onScaleStart : null,
onScaleUpdate: wantDrag ? onScaleUpdate : null,
onScaleEnd: wantDrag ? onScaleEnd : null,
);
}
void onTapDown(TapDownDetails d) {
final container = _containerResolver();
final localPosition = container.globalToLocal(d.globalPosition);
_lastTapPoint = new Point(localPosition.dx, localPosition.dy);
container.gestureProxy.onTapTest(_lastTapPoint);
// Kick off a timer to see if this is a LongPress.
if (_listeningForLongPress) {
_longPressTimer = new Timer(_kLongPressTimeout, () {
onLongPress();
_longPressTimer = null;
});
}
}
void onTapUp(TapUpDetails d) {
_longPressTimer?.cancel();
final container = _containerResolver();
final localPosition = container.globalToLocal(d.globalPosition);
_lastTapPoint = new Point(localPosition.dx, localPosition.dy);
container.gestureProxy.onTap(_lastTapPoint);
}
void onLongPress() {
final container = _containerResolver();
container.gestureProxy.onLongPress(_lastTapPoint);
}
void onScaleStart(ScaleStartDetails d) {
_longPressTimer?.cancel();
final container = _containerResolver();
final localPosition = container.globalToLocal(d.focalPoint);
_lastTapPoint = new Point(localPosition.dx, localPosition.dy);
_isDragging = container.gestureProxy.onDragStart(_lastTapPoint);
}
void onScaleUpdate(ScaleUpdateDetails d) {
if (!_isDragging) {
return;
}
final container = _containerResolver();
final localPosition = container.globalToLocal(d.focalPoint);
_lastTapPoint = new Point(localPosition.dx, localPosition.dy);
_lastScale = d.scale;
container.gestureProxy.onDragUpdate(_lastTapPoint, d.scale);
}
void onScaleEnd(ScaleEndDetails d) {
if (!_isDragging) {
return;
}
final container = _containerResolver();
container.gestureProxy
.onDragEnd(_lastTapPoint, _lastScale, d.velocity.pixelsPerSecond.dx);
}
}
// Exposed for testing.
typedef ChartContainerRenderObject _ContainerResolver();

View File

@@ -0,0 +1,36 @@
// 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.
abstract class ChartState {
void setAnimation(Duration transition);
/// Request to the native platform to rebuild the chart.
void requestRebuild();
/// Informs the chart that the configuration has changed.
///
/// This flag is set by checks that detect if a configuration has changed,
/// such as behaviors, axis, and renderers.
///
/// This flag is read on chart rebuild, if chart is marked as dirty, then the
/// chart will call a base chart draw.
void markChartDirty();
/// Reset the chart dirty flag.
void resetChartDirtyFlag();
/// Gets if the chart is dirty.
bool get chartIsDirty;
}

View File

@@ -0,0 +1,122 @@
// 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/common.dart' as common
show
AxisSpec,
NumericCartesianChart,
OrdinalCartesianChart,
RTLSpec,
Series,
SeriesRendererConfig;
import '../behaviors/chart_behavior.dart' show ChartBehavior;
import '../base_chart.dart' show LayoutConfig;
import '../base_chart_state.dart' show BaseChartState;
import '../cartesian_chart.dart' show CartesianChart;
import '../selection_model_config.dart' show SelectionModelConfig;
/// A numeric combo chart supports rendering each series of data with different
/// series renderers.
///
/// Note that if you have DateTime data, you should use [TimeSeriesChart]. We do
/// not expose a separate DateTimeComboChart because it would just be a copy of
/// that chart.
class NumericComboChart extends CartesianChart<num> {
NumericComboChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
common.SeriesRendererConfig<num> defaultRenderer,
List<common.SeriesRendererConfig<num>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<num>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
);
@override
common.NumericCartesianChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.NumericCartesianChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis());
}
}
/// An ordinal combo chart supports rendering each series of data with different
/// series renderers.
class OrdinalComboChart extends CartesianChart<String> {
OrdinalComboChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
common.SeriesRendererConfig<String> defaultRenderer,
List<common.SeriesRendererConfig<String>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<String>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
);
@override
common.OrdinalCartesianChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.OrdinalCartesianChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis());
}
}

View File

@@ -0,0 +1,50 @@
// 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/common.dart' as common
show GraphicsFactory, LineStyle, TextElement, TextStyle;
import 'package:flutter_web/widgets.dart' show BuildContext, MediaQuery;
import 'line_style.dart' show LineStyle;
import 'text_element.dart' show TextElement;
import 'text_style.dart' show TextStyle;
class GraphicsFactory implements common.GraphicsFactory {
final double textScaleFactor;
GraphicsFactory(BuildContext context,
{GraphicsFactoryHelper helper = const GraphicsFactoryHelper()})
: textScaleFactor = helper.getTextScaleFactorOf(context);
/// Returns a [TextStyle] object.
@override
common.TextStyle createTextPaint() => new TextStyle();
/// Returns a text element from [text] and [style].
@override
common.TextElement createTextElement(String text) {
return new TextElement(text, textScaleFactor: textScaleFactor);
}
@override
common.LineStyle createLinePaint() => new LineStyle();
}
/// Wraps the MediaQuery function to allow for testing.
class GraphicsFactoryHelper {
const GraphicsFactoryHelper();
double getTextScaleFactorOf(BuildContext context) =>
MediaQuery.textScaleFactorOf(context);
}

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 'dart:collection' show LinkedHashMap;
import 'package:charts_common/common.dart' as common
show
AxisSpec,
LineChart,
NumericAxisSpec,
RTLSpec,
Series,
LineRendererConfig,
SeriesRendererConfig;
import 'behaviors/line_point_highlighter.dart' show LinePointHighlighter;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'base_chart.dart' show LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'cartesian_chart.dart' show CartesianChart;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
class LineChart extends CartesianChart<num> {
LineChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes,
common.LineRendererConfig<num> defaultRenderer,
List<common.SeriesRendererConfig<num>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<num>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
bool flipVerticalAxis,
UserManagedState<num> userManagedState,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
flipVerticalAxis: flipVerticalAxis,
userManagedState: userManagedState,
);
@override
common.LineChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.LineChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(),
disjointMeasureAxes: createDisjointMeasureAxes());
}
@override
void addDefaultInteractions(List<ChartBehavior> behaviors) {
super.addDefaultInteractions(behaviors);
behaviors.add(new LinePointHighlighter());
}
}

View File

@@ -0,0 +1,25 @@
// 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/common.dart' as common show Color, LineStyle;
class LineStyle implements common.LineStyle {
@override
common.Color color;
@override
List<int> dashPattern;
@override
int strokeWidth;
}

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:charts_common/common.dart' as common
show ArcRendererConfig, PieChart, RTLSpec, Series;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'base_chart.dart' show BaseChart, LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'selection_model_config.dart' show SelectionModelConfig;
class PieChart<D> extends BaseChart<D> {
PieChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.ArcRendererConfig<D> defaultRenderer,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<D>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
defaultRenderer: defaultRenderer,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
);
@override
common.PieChart<D> createCommonChart(BaseChartState chartState) =>
new common.PieChart<D>(layoutConfig: layoutConfig?.commonLayoutConfig);
@override
void addDefaultInteractions(List<ChartBehavior> behaviors) {
super.addDefaultInteractions(behaviors);
}
}

View File

@@ -0,0 +1,82 @@
// 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 'package:charts_common/common.dart' as common
show
AxisSpec,
NumericAxisSpec,
PointRendererConfig,
RTLSpec,
ScatterPlotChart,
SeriesRendererConfig,
Series;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'base_chart.dart' show LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'cartesian_chart.dart' show CartesianChart;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
class ScatterPlotChart extends CartesianChart<num> {
ScatterPlotChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes,
common.PointRendererConfig<num> defaultRenderer,
List<common.SeriesRendererConfig<num>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<num>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
bool flipVerticalAxis,
UserManagedState<num> userManagedState,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
flipVerticalAxis: flipVerticalAxis,
userManagedState: userManagedState,
);
@override
common.ScatterPlotChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.ScatterPlotChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(),
disjointMeasureAxes: createDisjointMeasureAxes());
}
}

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 'package:meta/meta.dart' show immutable;
import 'package:charts_common/common.dart' as common;
@immutable
class SelectionModelConfig<D> {
final common.SelectionModelType type;
/// Listens for change in selection.
final common.SelectionModelListener<D> changedListener;
/// Listens anytime update selection is called.
final common.SelectionModelListener<D> updatedListener;
SelectionModelConfig(
{this.type = common.SelectionModelType.info,
this.changedListener,
this.updatedListener});
}

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 'dart:math' show Rectangle;
import 'package:charts_common/common.dart' as common
show ChartCanvas, Color, SymbolRenderer;
import 'package:flutter_web/widgets.dart';
import 'chart_canvas.dart' show ChartCanvas;
import 'graphics_factory.dart' show GraphicsFactory;
/// Flutter widget responsible for painting a common SymbolRenderer from the
/// chart.
///
/// If you want to customize the symbol, then use [CustomSymbolRenderer].
class SymbolRendererCanvas implements SymbolRendererBuilder {
final common.SymbolRenderer commonSymbolRenderer;
SymbolRendererCanvas(this.commonSymbolRenderer);
@override
Widget build(BuildContext context,
{Color color, Size size, bool enabled = true}) {
if (!enabled) {
color = color.withOpacity(0.26);
}
return new SizedBox.fromSize(
size: size,
child: new CustomPaint(
painter:
new _SymbolCustomPaint(context, commonSymbolRenderer, color)));
}
}
/// Convenience class allowing you to pass your Widget builder through the
/// common chart so that it is created for you by the Legend.
///
/// This allows a custom SymbolRenderer in Flutter without having to create
/// a completely custom legend.
abstract class CustomSymbolRenderer extends common.SymbolRenderer
implements SymbolRendererBuilder {
/// Must override this method to build the custom Widget with the given color
/// as
@override
Widget build(BuildContext context, {Color color, Size size, bool enabled});
@override
void paint(common.ChartCanvas canvas, Rectangle<num> bounds,
{List<int> dashPattern,
common.Color fillColor,
common.Color strokeColor,
double strokeWidthPx}) {
// Intentionally ignored (never called).
}
@override
bool shouldRepaint(common.SymbolRenderer oldRenderer) {
return false; // Repainting is handled directly in Flutter.
}
}
/// Common interface for [CustomSymbolRenderer] & [SymbolRendererCanvas] for
/// convenience for [LegendEntryLayout].
abstract class SymbolRendererBuilder {
Widget build(BuildContext context, {Color color, Size size, bool enabled});
}
/// The Widget which fulfills the guts of [SymbolRendererCanvas] actually
/// painting the symbol to a canvas using [CustomPainter].
class _SymbolCustomPaint extends CustomPainter {
final BuildContext context;
final common.SymbolRenderer symbolRenderer;
final Color color;
_SymbolCustomPaint(this.context, this.symbolRenderer, this.color);
@override
void paint(Canvas canvas, Size size) {
final bounds =
new Rectangle<num>(0, 0, size.width.toInt(), size.height.toInt());
final commonColor = new common.Color(
r: color.red, g: color.green, b: color.blue, a: color.alpha);
symbolRenderer.paint(
new ChartCanvas(canvas, GraphicsFactory(context)), bounds,
fillColor: commonColor, strokeColor: commonColor);
}
@override
bool shouldRepaint(_SymbolCustomPaint oldDelegate) {
return symbolRenderer.shouldRepaint(oldDelegate.symbolRenderer);
}
}

View File

@@ -0,0 +1,183 @@
// 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:flutter_web_ui/ui.dart' show TextAlign, TextDirection;
import 'package:charts_common/common.dart' as common
show
MaxWidthStrategy,
TextElement,
TextDirection,
TextMeasurement,
TextStyle;
import 'package:flutter_web/rendering.dart'
show Color, TextBaseline, TextPainter, TextSpan, TextStyle;
/// Flutter implementation for text measurement and painter.
class TextElement implements common.TextElement {
static const ellipsis = '\u{2026}';
@override
final String text;
final double textScaleFactor;
var _painterReady = false;
common.TextStyle _textStyle;
common.TextDirection _textDirection = common.TextDirection.ltr;
int _maxWidth;
common.MaxWidthStrategy _maxWidthStrategy;
TextPainter _textPainter;
common.TextMeasurement _measurement;
double _opacity;
TextElement(this.text, {common.TextStyle style, this.textScaleFactor})
: _textStyle = style;
@override
common.TextStyle get textStyle => _textStyle;
@override
set textStyle(common.TextStyle value) {
if (_textStyle == value) {
return;
}
_textStyle = value;
_painterReady = false;
}
@override
set textDirection(common.TextDirection direction) {
if (_textDirection == direction) {
return;
}
_textDirection = direction;
_painterReady = false;
}
@override
common.TextDirection get textDirection => _textDirection;
@override
int get maxWidth => _maxWidth;
@override
set maxWidth(int value) {
if (_maxWidth == value) {
return;
}
_maxWidth = value;
_painterReady = false;
}
@override
common.MaxWidthStrategy get maxWidthStrategy => _maxWidthStrategy;
@override
set maxWidthStrategy(common.MaxWidthStrategy maxWidthStrategy) {
if (_maxWidthStrategy == maxWidthStrategy) {
return;
}
_maxWidthStrategy = maxWidthStrategy;
_painterReady = false;
}
@override
set opacity(double opacity) {
if (opacity != _opacity) {
_painterReady = false;
_opacity = opacity;
}
}
@override
common.TextMeasurement get measurement {
if (!_painterReady) {
_refreshPainter();
}
return _measurement;
}
/// The estimated distance between where we asked to draw the text (top, left)
/// and where it visually started (top + verticalFontShift, left).
///
/// 10% of reported font height seems to be about right.
int get verticalFontShift {
if (!_painterReady) {
_refreshPainter();
}
return (_textPainter.height * 0.1).ceil();
}
TextPainter get textPainter {
if (!_painterReady) {
_refreshPainter();
}
return _textPainter;
}
/// Create text painter and measure based on current settings
void _refreshPainter() {
_opacity ??= 1.0;
var color = new Color.fromARGB(
(textStyle.color.a * _opacity).round(),
textStyle.color.r,
textStyle.color.g,
textStyle.color.b,
);
_textPainter = new TextPainter(
text: new TextSpan(
text: text,
style: new TextStyle(
color: color,
fontSize: textStyle.fontSize.toDouble(),
fontFamily: textStyle.fontFamily)))
..textDirection = TextDirection.ltr
// TODO Flip once textAlign works
..textAlign = TextAlign.left
// ..textAlign = _textDirection == common.TextDirection.rtl ?
// TextAlign.right : TextAlign.left
..ellipsis = maxWidthStrategy == common.MaxWidthStrategy.ellipsize
? ellipsis
: null;
if (textScaleFactor != null) {
_textPainter.textScaleFactor = textScaleFactor;
}
_textPainter.layout(maxWidth: maxWidth?.toDouble() ?? double.infinity);
final baseline =
_textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
// Estimating the actual draw height to 70% of measures size.
//
// The font reports a size larger than the drawn size, which makes it
// difficult to shift the text around to get it to visually line up
// vertically with other components.
_measurement = new common.TextMeasurement(
horizontalSliceWidth: _textPainter.width,
verticalSliceWidth: _textPainter.height * 0.70,
baseline: baseline);
_painterReady = true;
}
}

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:flutter_web_ui/ui.dart' show hashValues;
import 'package:charts_common/common.dart' as common show Color, TextStyle;
class TextStyle implements common.TextStyle {
int fontSize;
String fontFamily;
common.Color color;
@override
bool operator ==(Object other) =>
other is TextStyle &&
fontSize == other.fontSize &&
fontFamily == other.fontFamily &&
color == other.color;
@override
int get hashCode => hashValues(fontSize, fontFamily, color);
}

View File

@@ -0,0 +1,94 @@
// 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 'package:charts_common/common.dart' as common
show
AxisSpec,
DateTimeFactory,
NumericAxisSpec,
Series,
SeriesRendererConfig,
TimeSeriesChart;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'behaviors/line_point_highlighter.dart' show LinePointHighlighter;
import 'cartesian_chart.dart' show CartesianChart;
import 'base_chart.dart' show LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
class TimeSeriesChart extends CartesianChart<DateTime> {
final common.DateTimeFactory dateTimeFactory;
/// Create a [TimeSeriesChart].
///
/// [dateTimeFactory] allows specifying a factory that creates [DateTime] to
/// be used for the time axis. If none specified, local date time is used.
TimeSeriesChart(
List<common.Series<dynamic, DateTime>> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes,
common.SeriesRendererConfig<DateTime> defaultRenderer,
List<common.SeriesRendererConfig<DateTime>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<DateTime>> selectionModels,
LayoutConfig layoutConfig,
this.dateTimeFactory,
bool defaultInteractions: true,
bool flipVerticalAxis,
UserManagedState<DateTime> userManagedState,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
flipVerticalAxis: flipVerticalAxis,
userManagedState: userManagedState,
);
@override
common.TimeSeriesChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.TimeSeriesChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(),
disjointMeasureAxes: createDisjointMeasureAxes());
}
@override
void addDefaultInteractions(List<ChartBehavior> behaviors) {
super.addDefaultInteractions(behaviors);
behaviors.add(new LinePointHighlighter());
}
}

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 'package:charts_common/common.dart' as common
show ImmutableSeries, SelectionModel, SelectionModelType, SeriesDatumConfig;
/// Contains override settings for the internal chart state.
///
/// The chart will check non null settings and apply them if they differ from
/// the internal chart state and trigger the appropriate level of redrawing.
class UserManagedState<D> {
/// The expected selection(s) on the chart.
///
/// If this is set and the model for the selection model type differs from
/// what is in the internal chart state, the selection will be applied and
/// repainting will occur such that behaviors that draw differently on
/// selection change can update, such as the line point highlighter.
///
/// If more than one type of selection model is used, only the one(s)
/// specified in this list will override what is kept in the internally.
///
/// To clear the selection, add an empty selection model.
final selectionModels =
<common.SelectionModelType, UserManagedSelectionModel<D>>{};
}
/// Container for the user managed selection model.
///
/// This container is needed because the selection model generated by selection
/// events is a [SelectionModel], while any user defined selection has to be
/// specified by passing in [selectedSeriesConfig] and [selectedDataConfig].
/// The configuration is converted to a selection model after the series data
/// has been processed.
class UserManagedSelectionModel<D> {
final List<String> selectedSeriesConfig;
final List<common.SeriesDatumConfig> selectedDataConfig;
common.SelectionModel<D> _model;
/// Creates a [UserManagedSelectionModel] that holds [SelectionModel].
///
/// [selectedSeriesConfig] and [selectedDataConfig] is set to null because the
/// [_model] is returned when [getModel] is called.
UserManagedSelectionModel({common.SelectionModel<D> model})
: _model = model ?? new common.SelectionModel(),
selectedSeriesConfig = null,
selectedDataConfig = null;
/// Creates a [UserManagedSelectionModel] with configuration that is converted
/// to a [SelectionModel] when [getModel] provides a processed series list.
UserManagedSelectionModel.fromConfig(
{List<String> selectedSeriesConfig,
List<common.SeriesDatumConfig> selectedDataConfig})
: this.selectedSeriesConfig = selectedSeriesConfig ?? <String>[],
this.selectedDataConfig =
selectedDataConfig ?? <common.SeriesDatumConfig>[];
/// Gets the selection model. If the model is null, create one from
/// configuration and the processed [seriesList] passed in.
common.SelectionModel<D> getModel(
List<common.ImmutableSeries<D>> seriesList) {
_model ??= new common.SelectionModel.fromConfig(
selectedDataConfig, selectedSeriesConfig, seriesList);
return _model;
}
}

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:flutter_web/rendering.dart'
show
RenderBox,
RenderSemanticsGestureHandler,
RenderPointerListener,
RenderCustomMultiChildLayoutBox;
import 'chart_container.dart' show ChartContainerRenderObject;
/// Get the [ChartContainerRenderObject] from a [RenderBox].
///
/// [RenderBox] is expected to be a [RenderSemanticsGestureHandler] with child
/// of [RenderPointerListener] with child of [ChartContainerRenderObject].
ChartContainerRenderObject getChartContainerRenderObject(RenderBox box) {
assert(box is RenderCustomMultiChildLayoutBox);
final semanticHandler = (box as RenderCustomMultiChildLayoutBox)
.getChildrenAsList()
.firstWhere((child) => child is RenderSemanticsGestureHandler);
assert(semanticHandler is RenderSemanticsGestureHandler);
final renderPointerListener =
(semanticHandler as RenderSemanticsGestureHandler).child;
assert(renderPointerListener is RenderPointerListener);
final chartContainerRenderObject =
(renderPointerListener as RenderPointerListener).child;
assert(chartContainerRenderObject is ChartContainerRenderObject);
return chartContainerRenderObject as ChartContainerRenderObject;
}

View File

@@ -0,0 +1,28 @@
// 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/common.dart' as common show Color;
import 'package:flutter_web_ui/ui.dart' as ui;
class ColorUtil {
static ui.Color toDartColor(common.Color color) {
return ui.Color.fromARGB(color.a, color.r, color.g, color.b);
}
static common.Color fromDartColor(ui.Color color) {
return common.Color(
r: color.red, g: color.green, b: color.blue, a: color.alpha);
}
}

View File

@@ -0,0 +1,219 @@
// 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:flutter_web_ui/ui.dart' show Offset;
import 'package:flutter_web/material.dart';
import 'package:flutter_web/rendering.dart';
import 'package:flutter_web/widgets.dart';
import 'package:charts_common/common.dart' as common
show BehaviorPosition, InsideJustification, OutsideJustification;
import 'behaviors/chart_behavior.dart' show BuildableBehavior;
/// Layout delegate that layout chart widget with [BuildableBehavior] widgets.
class WidgetLayoutDelegate extends MultiChildLayoutDelegate {
/// ID of the common chart widget.
final String chartID;
/// Directionality of the widget.
final isRTL;
/// ID and [BuildableBehavior] of the widgets for calculating offset.
final Map<String, BuildableBehavior> idAndBehavior;
WidgetLayoutDelegate(this.chartID, this.idAndBehavior, this.isRTL);
@override
void performLayout(Size size) {
// TODO: Change this to a layout manager that supports more
// than one buildable behavior that changes chart size. Remove assert when
// this is possible.
assert(idAndBehavior.keys.isEmpty || idAndBehavior.keys.length == 1);
// Size available for the chart widget.
var availableWidth = size.width;
var availableHeight = size.height;
var chartOffset = Offset.zero;
// Measure the first buildable behavior.
final behaviorID =
idAndBehavior.keys.isNotEmpty ? idAndBehavior.keys.first : null;
var behaviorSize = Size.zero;
if (behaviorID != null) {
if (hasChild(behaviorID)) {
final leftPosition =
isRTL ? common.BehaviorPosition.end : common.BehaviorPosition.start;
final rightPosition =
isRTL ? common.BehaviorPosition.start : common.BehaviorPosition.end;
final behaviorPosition = idAndBehavior[behaviorID].position;
behaviorSize = layoutChild(behaviorID, new BoxConstraints.loose(size));
if (behaviorPosition == common.BehaviorPosition.top) {
chartOffset = new Offset(0.0, behaviorSize.height);
availableHeight -= behaviorSize.height;
} else if (behaviorPosition == common.BehaviorPosition.bottom) {
availableHeight -= behaviorSize.height;
} else if (behaviorPosition == leftPosition) {
chartOffset = new Offset(behaviorSize.width, 0.0);
availableWidth -= behaviorSize.width;
} else if (behaviorPosition == rightPosition) {
availableWidth -= behaviorSize.width;
}
}
}
// Layout chart.
final chartSize = new Size(availableWidth, availableHeight);
if (hasChild(chartID)) {
layoutChild(chartID, new BoxConstraints.tight(chartSize));
positionChild(chartID, chartOffset);
}
// Position buildable behavior.
if (behaviorID != null) {
// TODO: Unable to relayout with new smaller width.
// In the delegate, all children are required to have layout called
// exactly once.
final behaviorOffset = _getBehaviorOffset(idAndBehavior[behaviorID],
behaviorSize: behaviorSize, chartSize: chartSize, isRTL: isRTL);
positionChild(behaviorID, behaviorOffset);
}
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) {
// TODO: Deep equality check because the instance will not be
// the same on each build, even if the buildable behavior has not changed.
return idAndBehavior != (oldDelegate as WidgetLayoutDelegate).idAndBehavior;
}
// Calculate buildable behavior's offset.
Offset _getBehaviorOffset(BuildableBehavior behavior,
{Size behaviorSize, Size chartSize, bool isRTL}) {
Offset behaviorOffset;
final behaviorPosition = behavior.position;
final outsideJustification = behavior.outsideJustification;
final insideJustification = behavior.insideJustification;
if (behaviorPosition == common.BehaviorPosition.top ||
behaviorPosition == common.BehaviorPosition.bottom) {
final heightOffset = behaviorPosition == common.BehaviorPosition.bottom
? chartSize.height
: 0.0;
final horizontalJustification =
getOutsideJustification(outsideJustification, isRTL);
switch (horizontalJustification) {
case _HorizontalJustification.leftDrawArea:
behaviorOffset =
new Offset(behavior.drawAreaBounds.left.toDouble(), heightOffset);
break;
case _HorizontalJustification.left:
behaviorOffset = new Offset(0.0, heightOffset);
break;
case _HorizontalJustification.rightDrawArea:
behaviorOffset = new Offset(
behavior.drawAreaBounds.right - behaviorSize.width, heightOffset);
break;
case _HorizontalJustification.right:
behaviorOffset =
new Offset(chartSize.width - behaviorSize.width, heightOffset);
break;
}
} else if (behaviorPosition == common.BehaviorPosition.start ||
behaviorPosition == common.BehaviorPosition.end) {
final widthOffset =
(isRTL && behaviorPosition == common.BehaviorPosition.start) ||
(!isRTL && behaviorPosition == common.BehaviorPosition.end)
? chartSize.width
: 0.0;
switch (outsideJustification) {
case common.OutsideJustification.startDrawArea:
case common.OutsideJustification.middleDrawArea:
behaviorOffset =
new Offset(widthOffset, behavior.drawAreaBounds.top.toDouble());
break;
case common.OutsideJustification.start:
case common.OutsideJustification.middle:
behaviorOffset = new Offset(widthOffset, 0.0);
break;
case common.OutsideJustification.endDrawArea:
behaviorOffset = new Offset(widthOffset,
behavior.drawAreaBounds.bottom - behaviorSize.height);
break;
case common.OutsideJustification.end:
behaviorOffset =
new Offset(widthOffset, chartSize.height - behaviorSize.height);
break;
}
} else if (behaviorPosition == common.BehaviorPosition.inside) {
var rightOffset = new Offset(chartSize.width - behaviorSize.width, 0.0);
switch (insideJustification) {
case common.InsideJustification.topStart:
behaviorOffset = isRTL ? rightOffset : Offset.zero;
break;
case common.InsideJustification.topEnd:
behaviorOffset = isRTL ? Offset.zero : rightOffset;
break;
}
}
return behaviorOffset;
}
_HorizontalJustification getOutsideJustification(
common.OutsideJustification justification, bool isRTL) {
_HorizontalJustification mappedJustification;
switch (justification) {
case common.OutsideJustification.startDrawArea:
case common.OutsideJustification.middleDrawArea:
mappedJustification = isRTL
? _HorizontalJustification.rightDrawArea
: _HorizontalJustification.leftDrawArea;
break;
case common.OutsideJustification.start:
case common.OutsideJustification.middle:
mappedJustification = isRTL
? _HorizontalJustification.right
: _HorizontalJustification.left;
break;
case common.OutsideJustification.endDrawArea:
mappedJustification = isRTL
? _HorizontalJustification.leftDrawArea
: _HorizontalJustification.rightDrawArea;
break;
case common.OutsideJustification.end:
mappedJustification = isRTL
? _HorizontalJustification.left
: _HorizontalJustification.right;
break;
}
return mappedJustification;
}
}
enum _HorizontalJustification {
leftDrawArea,
left,
rightDrawArea,
right,
}