1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-09 14:28:51 +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,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,
}