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:
committed by
Andrew Brogdon
parent
42f2dce01b
commit
3fe927cb29
104
web/charts/flutter/lib/src/bar_chart.dart
Normal file
104
web/charts/flutter/lib/src/bar_chart.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
279
web/charts/flutter/lib/src/base_chart.dart
Normal file
279
web/charts/flutter/lib/src/base_chart.dart
Normal 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);
|
||||
}
|
||||
179
web/charts/flutter/lib/src/base_chart_state.dart
Normal file
179
web/charts/flutter/lib/src/base_chart_state.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
72
web/charts/flutter/lib/src/behaviors/chart_behavior.dart
Normal file
72
web/charts/flutter/lib/src/behaviors/chart_behavior.dart
Normal 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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
54
web/charts/flutter/lib/src/behaviors/domain_highlighter.dart
Normal file
54
web/charts/flutter/lib/src/behaviors/domain_highlighter.dart
Normal 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;
|
||||
}
|
||||
68
web/charts/flutter/lib/src/behaviors/initial_selection.dart
Normal file
68
web/charts/flutter/lib/src/behaviors/initial_selection.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
340
web/charts/flutter/lib/src/behaviors/legend/datum_legend.dart
Normal file
340
web/charts/flutter/lib/src/behaviors/legend/datum_legend.dart
Normal 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) {}
|
||||
}
|
||||
22
web/charts/flutter/lib/src/behaviors/legend/legend.dart
Normal file
22
web/charts/flutter/lib/src/behaviors/legend/legend.dart
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
158
web/charts/flutter/lib/src/behaviors/legend/legend_layout.dart
Normal file
158
web/charts/flutter/lib/src/behaviors/legend/legend_layout.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
382
web/charts/flutter/lib/src/behaviors/legend/series_legend.dart
Normal file
382
web/charts/flutter/lib/src/behaviors/legend/series_legend.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
127
web/charts/flutter/lib/src/behaviors/line_point_highlighter.dart
Normal file
127
web/charts/flutter/lib/src/behaviors/line_point_highlighter.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
117
web/charts/flutter/lib/src/behaviors/range_annotation.dart
Normal file
117
web/charts/flutter/lib/src/behaviors/range_annotation.dart
Normal 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);
|
||||
}
|
||||
147
web/charts/flutter/lib/src/behaviors/select_nearest.dart
Normal file
147
web/charts/flutter/lib/src/behaviors/select_nearest.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
196
web/charts/flutter/lib/src/behaviors/slider/slider.dart
Normal file
196
web/charts/flutter/lib/src/behaviors/slider/slider.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
53
web/charts/flutter/lib/src/behaviors/sliding_viewport.dart
Normal file
53
web/charts/flutter/lib/src/behaviors/sliding_viewport.dart
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
186
web/charts/flutter/lib/src/behaviors/zoom/pan_behavior.dart
Normal file
186
web/charts/flutter/lib/src/behaviors/zoom/pan_behavior.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
104
web/charts/flutter/lib/src/canvas/circle_sector_painter.dart
Normal file
104
web/charts/flutter/lib/src/canvas/circle_sector_painter.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
242
web/charts/flutter/lib/src/canvas/line_painter.dart
Normal file
242
web/charts/flutter/lib/src/canvas/line_painter.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
88
web/charts/flutter/lib/src/canvas/pie_painter.dart
Normal file
88
web/charts/flutter/lib/src/canvas/pie_painter.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
web/charts/flutter/lib/src/canvas/point_painter.dart
Normal file
56
web/charts/flutter/lib/src/canvas/point_painter.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
96
web/charts/flutter/lib/src/canvas/polygon_painter.dart
Normal file
96
web/charts/flutter/lib/src/canvas/polygon_painter.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
125
web/charts/flutter/lib/src/cartesian_chart.dart
Normal file
125
web/charts/flutter/lib/src/cartesian_chart.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
442
web/charts/flutter/lib/src/chart_canvas.dart
Normal file
442
web/charts/flutter/lib/src/chart_canvas.dart
Normal 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) {}
|
||||
}
|
||||
419
web/charts/flutter/lib/src/chart_container.dart
Normal file
419
web/charts/flutter/lib/src/chart_container.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
136
web/charts/flutter/lib/src/chart_gesture_detector.dart
Normal file
136
web/charts/flutter/lib/src/chart_gesture_detector.dart
Normal 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();
|
||||
36
web/charts/flutter/lib/src/chart_state.dart
Normal file
36
web/charts/flutter/lib/src/chart_state.dart
Normal 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;
|
||||
}
|
||||
122
web/charts/flutter/lib/src/combo_chart/combo_chart.dart
Normal file
122
web/charts/flutter/lib/src/combo_chart/combo_chart.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
50
web/charts/flutter/lib/src/graphics_factory.dart
Normal file
50
web/charts/flutter/lib/src/graphics_factory.dart
Normal 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);
|
||||
}
|
||||
90
web/charts/flutter/lib/src/line_chart.dart
Normal file
90
web/charts/flutter/lib/src/line_chart.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
25
web/charts/flutter/lib/src/line_style.dart
Normal file
25
web/charts/flutter/lib/src/line_style.dart
Normal 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;
|
||||
}
|
||||
54
web/charts/flutter/lib/src/pie_chart.dart
Normal file
54
web/charts/flutter/lib/src/pie_chart.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
82
web/charts/flutter/lib/src/scatter_plot_chart.dart
Normal file
82
web/charts/flutter/lib/src/scatter_plot_chart.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
34
web/charts/flutter/lib/src/selection_model_config.dart
Normal file
34
web/charts/flutter/lib/src/selection_model_config.dart
Normal 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});
|
||||
}
|
||||
104
web/charts/flutter/lib/src/symbol_renderer.dart
Normal file
104
web/charts/flutter/lib/src/symbol_renderer.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
183
web/charts/flutter/lib/src/text_element.dart
Normal file
183
web/charts/flutter/lib/src/text_element.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
33
web/charts/flutter/lib/src/text_style.dart
Normal file
33
web/charts/flutter/lib/src/text_style.dart
Normal 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);
|
||||
}
|
||||
94
web/charts/flutter/lib/src/time_series_chart.dart
Normal file
94
web/charts/flutter/lib/src/time_series_chart.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
78
web/charts/flutter/lib/src/user_managed_state.dart
Normal file
78
web/charts/flutter/lib/src/user_managed_state.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
45
web/charts/flutter/lib/src/util.dart
Normal file
45
web/charts/flutter/lib/src/util.dart
Normal 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;
|
||||
}
|
||||
28
web/charts/flutter/lib/src/util/color.dart
Normal file
28
web/charts/flutter/lib/src/util/color.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
219
web/charts/flutter/lib/src/widget_layout_delegate.dart
Normal file
219
web/charts/flutter/lib/src/widget_layout_delegate.dart
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user