1
0
mirror of https://github.com/flutter/samples.git synced 2026-03-29 07:41:41 +00:00

Add flutter_web samples (#75)

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

View File

@@ -0,0 +1,53 @@
# 0.6.0
* Bars can now be rendered on line charts.
* Negative measure values will now be rendered on bar charts as a separate stack from the positive
values.
* Added a Datum Legend, which displays one entry per value in the first series on the chart. This is
useful for pie and scatter plot charts.
* The AxisPosition enum in RTLSpec was refactored to AxisDirection to better reflect its effect on
swapping the positions of all start and end components, and not just positioning the measure axes.
* Added custom colors for line renderer area skirts and confidence intervals. A new "areaColorFn"
has been added to Series, and corresponding data to the datum. We could not use the fillColorFn for
these elements, because that color is already applied to the internal section of points on line
charts (including highlighter behaviors).
# 0.5.0
* SelectionModelConfig's listener parameter has been renamed to "changeListener". This is a breaking
change. Please rename any existing uses of the "listener" parameter to "changeListener". This was
named in order to add an additional listener "updateListener" that listens to any update requests,
regardless if the selection model has changed.
* CartesianChart's method getMeasureAxis(String axisId) has been changed to
getMeasureAxis({String axisId) so that getting the primary measure axis will not need passing any id
that does not match the secondary measure axis id. This affects users implementing custom behaviors
using the existing method.
# 0.4.0
* Fixed export file to export ChartsBehavior in the Flutter library instead of the one that resides
in charts_common. The charts_common behavior should not be used except internally in the
charts_flutter library. This is a breaking change if you are using charts_common behavior.
* Declare compatibility with Dart 2.
* BasicNumericTickFormatterSpec now takes in a callback instead of NumberFormat as the default
constructor. Use named constructor withNumberFormat instead. This is a breaking change.
* BarRendererConfig is no longer default of type String, please change current usage to
BarRendererConfig<String>. This is a breaking change.
* BarTargetLineRendererConfig is no longer default of type String, please change current usage to
BarTargetLineRendererConfig<String>. This is a breaking change.
# 0.3.0
* Simplified API by removing the requirement for specifying the datum type when creating a chart.
For example, previously to construct a bar chart the syntax was 'new BarChart<MyDatumType>()'.
The syntax is now cleaned up to be 'new BarChart()'. Please refer to the
[online gallery](https://google.github.io/charts/flutter/gallery.html) for the correct syntax.
* Added scatter plot charts
* Added tap to hide for legends
* Added support for rendering area skirts to line charts
* Added support for configurable fill colors to bar charts
# 0.2.0
* Update color palette. Please use MaterialPalette instead of QuantumPalette.
* Dart2 fixes
# 0.1.0
Initial release.

202
web/charts/flutter/LICENSE Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@@ -0,0 +1,14 @@
# Flutter Charting library
[![pub package](https://img.shields.io/pub/v/charts_flutter.svg)](https://pub.dartlang.org/packages/charts_flutter)
Material Design data visualization library written natively in Dart.
## Supported charts
See the [online gallery](https://google.github.io/charts/flutter/gallery.html).
## Using the library
The `/example/` folder inside `charts_flutter` in the [GitHub repo](https://github.com/google/charts)
contains a full Flutter app with many demo examples.

View File

@@ -0,0 +1,191 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export 'package:charts_common/common.dart'
show
boundsLineRadiusPxFnKey,
boundsLineRadiusPxKey,
measureAxisIdKey,
pointSymbolRendererFnKey,
pointSymbolRendererIdKey,
rendererIdKey,
AnnotationLabelAnchor,
AnnotationLabelDirection,
AnnotationLabelPosition,
ArcLabelDecorator,
ArcLabelLeaderLineStyleSpec,
ArcLabelPosition,
ArcRenderer,
ArcRendererConfig,
AutoDateTimeTickFormatterSpec,
AutoDateTimeTickProviderSpec,
Axis,
AxisDirection,
AxisSpec,
BarGroupingType,
BarLabelAnchor,
BarLabelDecorator,
BarLabelPosition,
BarLaneRendererConfig,
BarRenderer,
BarRendererConfig,
BarTargetLineRenderer,
BarTargetLineRendererConfig,
BaseCartesianRenderer,
BasicNumericTickFormatterSpec,
BasicNumericTickProviderSpec,
BasicOrdinalTickProviderSpec,
BasicOrdinalTickFormatterSpec,
BehaviorPosition,
BucketingAxisSpec,
BucketingNumericTickProviderSpec,
CartesianChart,
ChartCanvas,
ChartContext,
ChartTitleDirection,
CircleSymbolRenderer,
Color,
ComparisonPointsDecorator,
ConstCornerStrategy,
CornerStrategy,
CylinderSymbolRenderer,
DateTimeAxisSpec,
DateTimeEndPointsTickProviderSpec,
DateTimeExtents,
DateTimeFactory,
DateTimeTickFormatter,
DateTimeTickFormatterSpec,
DateTimeTickProviderSpec,
DayTickProviderSpec,
DomainFormatter,
EndPointsTimeAxisSpec,
ExploreModeTrigger,
FillPatternType,
GestureListener,
GraphicsFactory,
GridlineRendererSpec,
ImmutableSeries,
InsideJustification,
LayoutPosition,
LayoutViewPaintOrder,
LayoutViewPositionOrder,
LegendDefaultMeasure,
LegendTapHandling,
LineAnnotationSegment,
LinePointHighlighterFollowLineType,
LineRenderer,
LineRendererConfig,
LineStyleSpec,
LocalDateTimeFactory,
LockSelection,
MarginSpec,
MaterialPalette,
MaterialStyle,
MaxWidthStrategy,
MeasureFormatter,
NoCornerStrategy,
NoneRenderSpec,
NumericAxis,
NumericAxisSpec,
NumericCartesianChart,
NumericEndPointsTickProviderSpec,
NumericExtents,
NumericTickFormatterSpec,
NumericTickProviderSpec,
OrdinalAxis,
OrdinalAxisSpec,
OrdinalCartesianChart,
OrdinalTickFormatterSpec,
OrdinalTickProviderSpec,
OrdinalViewport,
OutsideJustification,
PanningCompletedCallback,
PercentAxisSpec,
PercentInjectorTotalType,
Performance,
PointRenderer,
PointRendererConfig,
PointRendererDecorator,
PointSymbolRenderer,
RangeAnnotationAxisType,
RangeAnnotationSegment,
RectSymbolRenderer,
RenderSpec,
RTLSpec,
SelectionModel,
SelectionModelListener,
SelectionModelType,
SelectionTrigger,
Series,
SeriesDatum,
SeriesDatumConfig,
SeriesRenderer,
SeriesRendererConfig,
SimpleTickFormatterBase,
SliderListenerCallback,
SliderListenerDragState,
SliderStyle,
SmallTickRendererSpec,
StaticDateTimeTickProviderSpec,
StaticNumericTickProviderSpec,
StaticOrdinalTickProviderSpec,
StyleFactory,
SymbolAnnotationRenderer,
SymbolAnnotationRendererConfig,
TextStyleSpec,
TickFormatter,
TickFormatterSpec,
TickLabelAnchor,
TickLabelJustification,
TickSpec,
TimeFormatterSpec,
TypedAccessorFn,
UTCDateTimeFactory,
ViewMargin,
VocalizationCallback;
export 'src/bar_chart.dart';
export 'src/base_chart.dart' show BaseChart, LayoutConfig;
export 'src/behaviors/a11y/domain_a11y_explore_behavior.dart'
show DomainA11yExploreBehavior;
export 'src/behaviors/chart_behavior.dart' show ChartBehavior;
export 'src/behaviors/domain_highlighter.dart' show DomainHighlighter;
export 'src/behaviors/initial_selection.dart' show InitialSelection;
export 'src/behaviors/calculation/percent_injector.dart' show PercentInjector;
export 'src/behaviors/chart_title/chart_title.dart' show ChartTitle;
export 'src/behaviors/legend/datum_legend.dart' show DatumLegend;
export 'src/behaviors/legend/legend_content_builder.dart'
show LegendContentBuilder, TabularLegendContentBuilder;
export 'src/behaviors/legend/legend_layout.dart'
show LegendLayout, TabularLegendLayout;
export 'src/behaviors/legend/series_legend.dart' show SeriesLegend;
export 'src/behaviors/line_point_highlighter.dart' show LinePointHighlighter;
export 'src/behaviors/range_annotation.dart' show RangeAnnotation;
export 'src/behaviors/select_nearest.dart' show SelectNearest;
export 'src/behaviors/sliding_viewport.dart' show SlidingViewport;
export 'src/behaviors/slider/slider.dart' show Slider;
export 'src/behaviors/zoom/initial_hint_behavior.dart' show InitialHintBehavior;
export 'src/behaviors/zoom/pan_and_zoom_behavior.dart' show PanAndZoomBehavior;
export 'src/behaviors/zoom/pan_behavior.dart' show PanBehavior;
export 'src/combo_chart/combo_chart.dart';
export 'src/line_chart.dart';
export 'src/pie_chart.dart';
export 'src/scatter_plot_chart.dart';
export 'src/selection_model_config.dart' show SelectionModelConfig;
export 'src/symbol_renderer.dart' show CustomSymbolRenderer;
export 'src/time_series_chart.dart';
export 'src/user_managed_state.dart'
show UserManagedState, UserManagedSelectionModel;
export 'src/util/color.dart' show ColorUtil;

View File

@@ -0,0 +1,104 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap;
import 'package:charts_common/common.dart' as common
show
AxisSpec,
BarChart,
BarGroupingType,
BarRendererConfig,
BarRendererDecorator,
NumericAxisSpec,
RTLSpec,
Series,
SeriesRendererConfig;
import 'behaviors/domain_highlighter.dart' show DomainHighlighter;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'package:meta/meta.dart' show immutable;
import 'base_chart.dart' show LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'cartesian_chart.dart' show CartesianChart;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
@immutable
class BarChart extends CartesianChart<String> {
final bool vertical;
final common.BarRendererDecorator barRendererDecorator;
BarChart(
List<common.Series<dynamic, String>> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes,
common.BarGroupingType barGroupingType,
common.BarRendererConfig<String> defaultRenderer,
List<common.SeriesRendererConfig<String>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<String>> selectionModels,
common.RTLSpec rtlSpec,
this.vertical: true,
bool defaultInteractions: true,
LayoutConfig layoutConfig,
UserManagedState<String> userManagedState,
this.barRendererDecorator,
bool flipVerticalAxis,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes,
defaultRenderer: defaultRenderer ??
new common.BarRendererConfig<String>(
groupingType: barGroupingType,
barRendererDecorator: barRendererDecorator),
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
defaultInteractions: defaultInteractions,
layoutConfig: layoutConfig,
userManagedState: userManagedState,
flipVerticalAxis: flipVerticalAxis,
);
@override
common.BarChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.BarChart(
vertical: vertical,
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(),
disjointMeasureAxes: createDisjointMeasureAxes());
}
@override
void addDefaultInteractions(List<ChartBehavior> behaviors) {
super.addDefaultInteractions(behaviors);
behaviors.add(new DomainHighlighter());
}
}

View File

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

View File

@@ -0,0 +1,179 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter_web_ui/ui.dart' show TextDirection;
import 'package:flutter_web/material.dart'
show
AnimationController,
BuildContext,
State,
TickerProviderStateMixin,
Widget;
import 'package:charts_common/common.dart' as common;
import 'package:flutter_web/widgets.dart'
show Directionality, LayoutId, CustomMultiChildLayout;
import 'behaviors/chart_behavior.dart'
show BuildableBehavior, ChartBehavior, ChartStateBehavior;
import 'base_chart.dart' show BaseChart;
import 'chart_container.dart' show ChartContainer;
import 'chart_state.dart' show ChartState;
import 'chart_gesture_detector.dart' show ChartGestureDetector;
import 'widget_layout_delegate.dart';
class BaseChartState<D> extends State<BaseChart<D>>
with TickerProviderStateMixin
implements ChartState {
// Animation
AnimationController _animationController;
double _animationValue = 0.0;
Widget _oldWidget;
ChartGestureDetector _chartGestureDetector;
bool _configurationChanged = false;
final autoBehaviorWidgets = <ChartBehavior>[];
final addedBehaviorWidgets = <ChartBehavior>[];
final addedCommonBehaviorsByRole = <String, common.ChartBehavior>{};
final addedSelectionChangedListenersByType =
<common.SelectionModelType, common.SelectionModelListener<D>>{};
final addedSelectionUpdatedListenersByType =
<common.SelectionModelType, common.SelectionModelListener<D>>{};
final _behaviorAnimationControllers =
<ChartStateBehavior, AnimationController>{};
static const chartContainerLayoutID = 'chartContainer';
@override
void initState() {
super.initState();
_animationController = new AnimationController(vsync: this)
..addListener(_animationTick);
}
@override
void requestRebuild() {
setState(() {});
}
@override
void markChartDirty() {
_configurationChanged = true;
}
@override
void resetChartDirtyFlag() {
_configurationChanged = false;
}
@override
bool get chartIsDirty => _configurationChanged;
/// Builds the common chart canvas widget.
Widget _buildChartContainer() {
final chartContainer = new ChartContainer<D>(
oldChartWidget: _oldWidget,
chartWidget: widget,
chartState: this,
animationValue: _animationValue,
rtl: Directionality.of(context) == TextDirection.rtl,
rtlSpec: widget.rtlSpec,
userManagedState: widget.userManagedState,
);
_oldWidget = widget;
final desiredGestures = widget.getDesiredGestures(this);
if (desiredGestures.isNotEmpty) {
_chartGestureDetector ??= new ChartGestureDetector();
return _chartGestureDetector.makeWidget(
context, chartContainer, desiredGestures);
} else {
return chartContainer;
}
}
@override
Widget build(BuildContext context) {
final chartWidgets = <LayoutId>[];
final idAndBehaviorMap = <String, BuildableBehavior>{};
// Add the common chart canvas widget.
chartWidgets.add(new LayoutId(
id: chartContainerLayoutID, child: _buildChartContainer()));
// Add widget for each behavior that can build widgets
addedCommonBehaviorsByRole.forEach((id, behavior) {
if (behavior is BuildableBehavior) {
assert(id != chartContainerLayoutID);
final buildableBehavior = behavior as BuildableBehavior;
idAndBehaviorMap[id] = buildableBehavior;
final widget = buildableBehavior.build(context);
chartWidgets.add(new LayoutId(id: id, child: widget));
}
});
final isRTL = Directionality.of(context) == TextDirection.rtl;
return new CustomMultiChildLayout(
delegate: new WidgetLayoutDelegate(
chartContainerLayoutID, idAndBehaviorMap, isRTL),
children: chartWidgets);
}
@override
void dispose() {
_animationController.dispose();
_behaviorAnimationControllers
.forEach((_, controller) => controller?.dispose());
_behaviorAnimationControllers.clear();
super.dispose();
}
@override
void setAnimation(Duration transition) {
_playAnimation(transition);
}
void _playAnimation(Duration duration) {
_animationController.duration = duration;
_animationController.forward(from: (duration == Duration.zero) ? 1.0 : 0.0);
_animationValue = _animationController.value;
}
void _animationTick() {
setState(() {
_animationValue = _animationController.value;
});
}
/// Get animation controller to be used by [behavior].
AnimationController getAnimationController(ChartStateBehavior behavior) {
_behaviorAnimationControllers[behavior] ??=
new AnimationController(vsync: this);
return _behaviorAnimationControllers[behavior];
}
/// Dispose of animation controller used by [behavior].
void disposeAnimationController(ChartStateBehavior behavior) {
final controller = _behaviorAnimationControllers.remove(behavior);
controller?.dispose();
}
}

View File

@@ -0,0 +1,112 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show DomainA11yExploreBehavior, VocalizationCallback, ExploreModeTrigger;
import 'package:flutter_web/widgets.dart' show hashValues;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
/// Behavior that generates semantic nodes for each domain.
class DomainA11yExploreBehavior
extends ChartBehavior<common.DomainA11yExploreBehavior> {
/// Returns a string for a11y vocalization from a list of series datum.
final common.VocalizationCallback vocalizationCallback;
final Set<GestureType> desiredGestures;
/// The gesture that activates explore mode. Defaults to long press.
///
/// Turning on explore mode asks this [A11yBehavior] to generate nodes within
/// this chart.
final common.ExploreModeTrigger exploreModeTrigger;
/// Minimum width of the bounding box for the a11y focus.
///
/// Must be 1 or higher because invisible semantic nodes should not be added.
final double minimumWidth;
/// Optionally notify the OS when explore mode is enabled.
final String exploreModeEnabledAnnouncement;
/// Optionally notify the OS when explore mode is disabled.
final String exploreModeDisabledAnnouncement;
DomainA11yExploreBehavior._internal(
{this.vocalizationCallback,
this.exploreModeTrigger,
this.desiredGestures,
this.minimumWidth,
this.exploreModeEnabledAnnouncement,
this.exploreModeDisabledAnnouncement});
factory DomainA11yExploreBehavior(
{common.VocalizationCallback vocalizationCallback,
common.ExploreModeTrigger exploreModeTrigger,
double minimumWidth,
String exploreModeEnabledAnnouncement,
String exploreModeDisabledAnnouncement}) {
final desiredGestures = new Set<GestureType>();
exploreModeTrigger ??= common.ExploreModeTrigger.pressHold;
switch (exploreModeTrigger) {
case common.ExploreModeTrigger.pressHold:
desiredGestures..add(GestureType.onLongPress);
break;
case common.ExploreModeTrigger.tap:
desiredGestures..add(GestureType.onTap);
break;
}
return new DomainA11yExploreBehavior._internal(
vocalizationCallback: vocalizationCallback,
desiredGestures: desiredGestures,
exploreModeTrigger: exploreModeTrigger,
minimumWidth: minimumWidth,
exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement,
exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement,
);
}
@override
common.DomainA11yExploreBehavior<D> createCommonBehavior<D>() {
return new common.DomainA11yExploreBehavior<D>(
vocalizationCallback: vocalizationCallback,
exploreModeTrigger: exploreModeTrigger,
minimumWidth: minimumWidth,
exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement,
exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement);
}
@override
void updateCommonBehavior(common.DomainA11yExploreBehavior commonBehavior) {}
@override
String get role => 'DomainA11yExplore-${exploreModeTrigger}';
@override
bool operator ==(Object o) =>
o is DomainA11yExploreBehavior &&
vocalizationCallback == o.vocalizationCallback &&
exploreModeTrigger == o.exploreModeTrigger &&
minimumWidth == o.minimumWidth &&
exploreModeEnabledAnnouncement == o.exploreModeEnabledAnnouncement &&
exploreModeDisabledAnnouncement == o.exploreModeDisabledAnnouncement;
@override
int get hashCode {
return hashValues(minimumWidth, vocalizationCallback, exploreModeTrigger,
exploreModeEnabledAnnouncement, exploreModeDisabledAnnouncement);
}
}

View File

@@ -0,0 +1,72 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show PercentInjector, PercentInjectorTotalType;
import 'package:meta/meta.dart' show immutable;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that can inject series or domain percentages into each datum.
///
/// [totalType] configures the type of total to be calculated.
///
/// The measure values of each datum will be replaced by the percent of the
/// total measure value that each represents. The "raw" measure accessor
/// function on [MutableSeries] can still be used to get the original values.
///
/// Note that the results for measureLowerBound and measureUpperBound are not
/// currently well defined when converted into percentage values. This behavior
/// will replace them as percents to prevent bad axis results, but no effort is
/// made to bound them to within a "0 to 100%" data range.
///
/// Note that if the chart has a [Legend] that is capable of hiding series data,
/// then this behavior must be added after the [Legend] to ensure that it
/// calculates values after series have been potentially removed from the list.
@immutable
class PercentInjector extends ChartBehavior<common.PercentInjector> {
final desiredGestures = new Set<GestureType>();
/// The type of data total to be calculated.
final common.PercentInjectorTotalType totalType;
PercentInjector._internal({this.totalType});
/// Constructs a [PercentInjector].
///
/// [totalType] configures the type of data total to be calculated.
factory PercentInjector({common.PercentInjectorTotalType totalType}) {
totalType ??= common.PercentInjectorTotalType.domain;
return new PercentInjector._internal(totalType: totalType);
}
@override
common.PercentInjector<D> createCommonBehavior<D>() =>
new common.PercentInjector<D>(totalType: totalType);
@override
void updateCommonBehavior(common.PercentInjector commonBehavior) {}
@override
String get role => 'PercentInjector';
@override
bool operator ==(Object o) {
return o is PercentInjector && totalType == o.totalType;
}
@override
int get hashCode => totalType.hashCode;
}

View File

@@ -0,0 +1,72 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:charts_common/common.dart' as common
show
BehaviorPosition,
InsideJustification,
OutsideJustification,
ChartBehavior;
import 'package:meta/meta.dart' show immutable;
import 'package:flutter_web/widgets.dart' show BuildContext, Widget;
import '../base_chart_state.dart' show BaseChartState;
/// Flutter wrapper for chart behaviors.
@immutable
abstract class ChartBehavior<B extends common.ChartBehavior> {
Set<GestureType> get desiredGestures;
B createCommonBehavior<D>();
void updateCommonBehavior(B commonBehavior);
String get role;
}
/// A chart behavior that depends on Flutter [State].
abstract class ChartStateBehavior<B extends common.ChartBehavior> {
set chartState(BaseChartState chartState);
}
/// A chart behavior that can build a Flutter [Widget].
abstract class BuildableBehavior<B extends common.ChartBehavior> {
/// Builds a [Widget] based on the information passed in.
///
/// [context] Flutter build context for extracting inherited properties such
/// as Directionality.
Widget build(BuildContext context);
/// The position on the widget.
common.BehaviorPosition get position;
/// Justification of the widget, if [position] is top, bottom, start, or end.
common.OutsideJustification get outsideJustification;
/// Justification of the widget if [position] is [common.BehaviorPosition.inside].
common.InsideJustification get insideJustification;
/// Chart's draw area bounds are used for positioning.
Rectangle<int> get drawAreaBounds;
}
/// Types of gestures accepted by a chart.
enum GestureType {
onLongPress,
onTap,
onHover,
onDrag,
}

View File

@@ -0,0 +1,200 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show
BehaviorPosition,
ChartTitle,
ChartTitleDirection,
MaxWidthStrategy,
OutsideJustification,
TextStyleSpec;
import 'package:flutter_web/widgets.dart' show hashValues;
import 'package:meta/meta.dart' show immutable;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that adds a ChartTitle widget to a chart.
@immutable
class ChartTitle extends ChartBehavior<common.ChartTitle> {
final desiredGestures = new Set<GestureType>();
final common.BehaviorPosition behaviorPosition;
/// Minimum size of the legend component. Optional.
///
/// If the legend is positioned in the top or bottom margin, then this
/// configures the legend's height. If positioned in the start or end
/// position, this configures the legend's width.
final int layoutMinSize;
/// Preferred size of the legend component. Defaults to 0.
///
/// If the legend is positioned in the top or bottom margin, then this
/// configures the legend's height. If positioned in the start or end
/// position, this configures the legend's width.
final int layoutPreferredSize;
/// Strategy for handling title text that is too large to fit. Defaults to
/// truncating the text with ellipses.
final common.MaxWidthStrategy maxWidthStrategy;
/// Primary text for the title.
final String title;
/// Direction of the chart title text.
///
/// This defaults to horizontal for a title in the top or bottom
/// [behaviorPosition], or vertical for start or end [behaviorPosition].
final common.ChartTitleDirection titleDirection;
/// Justification of the title text if it is positioned outside of the draw
/// area.
final common.OutsideJustification titleOutsideJustification;
/// Space between the title and sub-title text, if defined.
///
/// This padding is not used if no sub-title is provided.
final int titlePadding;
/// Style of the [title] text.
final common.TextStyleSpec titleStyleSpec;
/// Secondary text for the sub-title.
///
/// [subTitle] is rendered on a second line below the [title], and may be
/// styled differently.
final String subTitle;
/// Style of the [subTitle] text.
final common.TextStyleSpec subTitleStyleSpec;
/// Space between the "inside" of the chart, and the title behavior itself.
///
/// This padding is applied to all the edge of the title that is in the
/// direction of the draw area. For a top positioned title, this is applied
/// to the bottom edge. [outerPadding] is applied to the top, left, and right
/// edges.
///
/// If a sub-title is defined, this is the space between the sub-title text
/// and the inside of the chart. Otherwise, it is the space between the title
/// text and the inside of chart.
final int innerPadding;
/// Space between the "outside" of the chart, and the title behavior itself.
///
/// This padding is applied to all 3 edges of the title that are not in the
/// direction of the draw area. For a top positioned title, this is applied
/// to the top, left, and right edges. [innerPadding] is applied to the
/// bottom edge.
final int outerPadding;
/// Constructs a [ChartTitle].
///
/// [title] primary text for the title.
///
/// [behaviorPosition] layout position for the title. Defaults to the top of
/// the chart.
///
/// [innerPadding] space between the "inside" of the chart, and the title
/// behavior itself.
///
/// [maxWidthStrategy] strategy for handling title text that is too large to
/// fit. Defaults to truncating the text with ellipses.
///
/// [titleDirection] direction of the chart title text.
///
/// [titleOutsideJustification] Justification of the title text if it is
/// positioned outside of the draw. Defaults to the middle of the margin area.
///
/// [titlePadding] space between the title and sub-title text, if defined.
///
/// [titleStyleSpec] style of the [title] text.
///
/// [subTitle] secondary text for the sub-title. Optional.
///
/// [subTitleStyleSpec] style of the [subTitle] text.
ChartTitle(this.title,
{this.behaviorPosition,
this.innerPadding,
this.layoutMinSize,
this.layoutPreferredSize,
this.outerPadding,
this.maxWidthStrategy,
this.titleDirection,
this.titleOutsideJustification,
this.titlePadding,
this.titleStyleSpec,
this.subTitle,
this.subTitleStyleSpec});
@override
common.ChartTitle<D> createCommonBehavior<D>() =>
new common.ChartTitle<D>(title,
behaviorPosition: behaviorPosition,
innerPadding: innerPadding,
layoutMinSize: layoutMinSize,
layoutPreferredSize: layoutPreferredSize,
outerPadding: outerPadding,
maxWidthStrategy: maxWidthStrategy,
titleDirection: titleDirection,
titleOutsideJustification: titleOutsideJustification,
titlePadding: titlePadding,
titleStyleSpec: titleStyleSpec,
subTitle: subTitle,
subTitleStyleSpec: subTitleStyleSpec);
@override
void updateCommonBehavior(common.ChartTitle commonBehavior) {}
@override
String get role => 'ChartTitle-${behaviorPosition.toString()}';
@override
bool operator ==(Object o) {
return o is ChartTitle &&
behaviorPosition == o.behaviorPosition &&
layoutMinSize == o.layoutMinSize &&
layoutPreferredSize == o.layoutPreferredSize &&
maxWidthStrategy == o.maxWidthStrategy &&
title == o.title &&
titleDirection == o.titleDirection &&
titleOutsideJustification == o.titleOutsideJustification &&
titleStyleSpec == o.titleStyleSpec &&
subTitle == o.subTitle &&
subTitleStyleSpec == o.subTitleStyleSpec &&
innerPadding == o.innerPadding &&
titlePadding == o.titlePadding &&
outerPadding == o.outerPadding;
}
@override
int get hashCode {
return hashValues(
behaviorPosition,
layoutMinSize,
layoutPreferredSize,
maxWidthStrategy,
title,
titleDirection,
titleOutsideJustification,
titleStyleSpec,
subTitle,
subTitleStyleSpec,
innerPadding,
titlePadding,
outerPadding);
}
}

View File

@@ -0,0 +1,54 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show DomainHighlighter, SelectionModelType;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that monitors the specified [SelectionModel] and darkens the
/// color for selected data.
///
/// This is typically used for bars and pies to highlight segments.
///
/// It is used in combination with SelectNearest to update the selection model
/// and expand selection out to the domain value.
@immutable
class DomainHighlighter extends ChartBehavior<common.DomainHighlighter> {
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
DomainHighlighter([this.selectionModelType = common.SelectionModelType.info]);
@override
common.DomainHighlighter<D> createCommonBehavior<D>() =>
new common.DomainHighlighter<D>(selectionModelType);
@override
void updateCommonBehavior(common.DomainHighlighter commonBehavior) {}
@override
String get role => 'domainHighlight-${selectionModelType.toString()}';
@override
bool operator ==(Object o) =>
o is DomainHighlighter && selectionModelType == o.selectionModelType;
@override
int get hashCode => selectionModelType.hashCode;
}

View File

@@ -0,0 +1,68 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:collection/collection.dart' show ListEquality;
import 'package:charts_common/common.dart' as common
show InitialSelection, SeriesDatumConfig, SelectionModelType;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that sets the initial selection for a [selectionModelType].
@immutable
class InitialSelection extends ChartBehavior<common.InitialSelection> {
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
final List<String> selectedSeriesConfig;
final List<common.SeriesDatumConfig> selectedDataConfig;
InitialSelection(
{this.selectionModelType = common.SelectionModelType.info,
this.selectedSeriesConfig,
this.selectedDataConfig});
@override
common.InitialSelection<D> createCommonBehavior<D>() =>
new common.InitialSelection<D>(
selectionModelType: selectionModelType,
selectedDataConfig: selectedDataConfig,
selectedSeriesConfig: selectedSeriesConfig);
@override
void updateCommonBehavior(common.InitialSelection commonBehavior) {}
@override
String get role => 'InitialSelection-${selectionModelType.toString()}';
@override
bool operator ==(Object o) {
return o is InitialSelection &&
selectionModelType == o.selectionModelType &&
new ListEquality()
.equals(selectedSeriesConfig, o.selectedSeriesConfig) &&
new ListEquality().equals(selectedDataConfig, o.selectedDataConfig);
}
@override
int get hashCode {
int hashcode = selectionModelType.hashCode;
hashcode = hashcode * 37 + (selectedSeriesConfig?.hashCode ?? 0);
hashcode = hashcode * 37 + (selectedDataConfig?.hashCode ?? 0);
return hashcode;
}
}

View File

@@ -0,0 +1,340 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show
BehaviorPosition,
DatumLegend,
InsideJustification,
LegendEntry,
MeasureFormatter,
LegendDefaultMeasure,
OutsideJustification,
SelectionModelType,
TextStyleSpec;
import 'package:flutter_web/widgets.dart'
show BuildContext, EdgeInsets, Widget, hashValues;
import 'package:meta/meta.dart' show immutable;
import '../../chart_container.dart' show ChartContainerRenderObject;
import '../chart_behavior.dart'
show BuildableBehavior, ChartBehavior, GestureType;
import 'legend.dart' show TappableLegend;
import 'legend_content_builder.dart'
show LegendContentBuilder, TabularLegendContentBuilder;
import 'legend_layout.dart' show TabularLegendLayout;
/// Datum legend behavior for charts.
///
/// By default this behavior creates one legend entry per datum in the first
/// series rendered on the chart.
@immutable
class DatumLegend extends ChartBehavior<common.DatumLegend> {
static const defaultBehaviorPosition = common.BehaviorPosition.top;
static const defaultOutsideJustification =
common.OutsideJustification.startDrawArea;
static const defaultInsideJustification = common.InsideJustification.topStart;
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
/// Builder for creating custom legend content.
final LegendContentBuilder contentBuilder;
/// Position of the legend relative to the chart.
final common.BehaviorPosition position;
/// Justification of the legend relative to the chart
final common.OutsideJustification outsideJustification;
final common.InsideJustification insideJustification;
/// Whether or not the legend should show measures.
///
/// By default this is false, measures are not shown. When set to true, the
/// default behavior is to show measure only if there is selected data.
/// Please set [legendDefaultMeasure] to something other than none to enable
/// showing measures when there is no selection.
///
/// This flag is used by the [contentBuilder], so a custom content builder
/// has to choose if it wants to use this flag.
final bool showMeasures;
/// Option to show measures when selection is null.
///
/// By default this is set to none, so no measures are shown when there is
/// no selection.
final common.LegendDefaultMeasure legendDefaultMeasure;
/// Formatter for measure value(s) if the measures are shown on the legend.
final common.MeasureFormatter measureFormatter;
/// Formatter for secondary measure value(s) if the measures are shown on the
/// legend and the series uses the secondary axis.
final common.MeasureFormatter secondaryMeasureFormatter;
/// Styles for legend entry label text.
final common.TextStyleSpec entryTextStyle;
static const defaultCellPadding = const EdgeInsets.all(8.0);
/// Create a new tabular layout legend.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [horizontalFirst] if true, legend entries will grow horizontally first
/// instead of vertically first. If the position is top, bottom, or inside,
/// this defaults to true. Otherwise false.
///
/// [desiredMaxRows] the max rows to use before layout out items in a new
/// column. By default there is no limit. The max columns created is the
/// smaller of desiredMaxRows and number of legend entries.
///
/// [desiredMaxColumns] the max columns to use before laying out items in a
/// new row. By default there is no limit. The max columns created is the
/// smaller of desiredMaxColumns and number of legend entries.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory DatumLegend({
common.BehaviorPosition position,
common.OutsideJustification outsideJustification,
common.InsideJustification insideJustification,
bool horizontalFirst,
int desiredMaxRows,
int desiredMaxColumns,
EdgeInsets cellPadding,
bool showMeasures,
common.LegendDefaultMeasure legendDefaultMeasure,
common.MeasureFormatter measureFormatter,
common.MeasureFormatter secondaryMeasureFormatter,
common.TextStyleSpec entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
cellPadding ??= defaultCellPadding;
// Set the tabular layout settings to match the position if it is not
// specified.
horizontalFirst ??= (position == common.BehaviorPosition.top ||
position == common.BehaviorPosition.bottom ||
position == common.BehaviorPosition.inside);
final layoutBuilder = horizontalFirst
? new TabularLegendLayout.horizontalFirst(
desiredMaxColumns: desiredMaxColumns, cellPadding: cellPadding)
: new TabularLegendLayout.verticalFirst(
desiredMaxRows: desiredMaxRows, cellPadding: cellPadding);
return new DatumLegend._internal(
contentBuilder:
new TabularLegendContentBuilder(legendLayout: layoutBuilder),
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle);
}
/// Create a legend with custom layout.
///
/// By default, the legend is place above the chart and horizontally aligned
/// to the start of the draw area.
///
/// [contentBuilder] builder for the custom layout.
///
/// [position] the legend will be positioned relative to the chart. Default
/// position is top.
///
/// [outsideJustification] justification of the legend relative to the chart
/// if the position is top, bottom, left, right. Default to start of the draw
/// area.
///
/// [insideJustification] justification of the legend relative to the chart if
/// the position is inside. Default to top of the chart, start of draw area.
/// Start of draw area means left for LTR directionality, and right for RTL.
///
/// [showMeasures] show measure values for each series.
///
/// [legendDefaultMeasure] if measure should show when there is no selection.
/// This is set to none by default (only shows measure for selected data).
///
/// [measureFormatter] formats measure value if measures are shown.
///
/// [secondaryMeasureFormatter] formats measures if measures are shown for the
/// series that uses secondary measure axis.
factory DatumLegend.customLayout(
LegendContentBuilder contentBuilder, {
common.BehaviorPosition position,
common.OutsideJustification outsideJustification,
common.InsideJustification insideJustification,
bool showMeasures,
common.LegendDefaultMeasure legendDefaultMeasure,
common.MeasureFormatter measureFormatter,
common.MeasureFormatter secondaryMeasureFormatter,
common.TextStyleSpec entryTextStyle,
}) {
// Set defaults if empty.
position ??= defaultBehaviorPosition;
outsideJustification ??= defaultOutsideJustification;
insideJustification ??= defaultInsideJustification;
return new DatumLegend._internal(
contentBuilder: contentBuilder,
selectionModelType: common.SelectionModelType.info,
position: position,
outsideJustification: outsideJustification,
insideJustification: insideJustification,
showMeasures: showMeasures ?? false,
legendDefaultMeasure:
legendDefaultMeasure ?? common.LegendDefaultMeasure.none,
measureFormatter: measureFormatter,
secondaryMeasureFormatter: secondaryMeasureFormatter,
entryTextStyle: entryTextStyle,
);
}
DatumLegend._internal({
this.contentBuilder,
this.selectionModelType,
this.position,
this.outsideJustification,
this.insideJustification,
this.showMeasures,
this.legendDefaultMeasure,
this.measureFormatter,
this.secondaryMeasureFormatter,
this.entryTextStyle,
});
@override
common.DatumLegend<D> createCommonBehavior<D>() =>
new _FlutterDatumLegend<D>(this);
@override
void updateCommonBehavior(common.DatumLegend commonBehavior) {
(commonBehavior as _FlutterDatumLegend).config = this;
}
/// All Legend behaviors get the same role ID, because you should only have
/// one legend on a chart.
@override
String get role => 'legend';
@override
bool operator ==(Object o) {
return o is DatumLegend &&
selectionModelType == o.selectionModelType &&
contentBuilder == o.contentBuilder &&
position == o.position &&
outsideJustification == o.outsideJustification &&
insideJustification == o.insideJustification &&
showMeasures == o.showMeasures &&
legendDefaultMeasure == o.legendDefaultMeasure &&
measureFormatter == o.measureFormatter &&
secondaryMeasureFormatter == o.secondaryMeasureFormatter &&
entryTextStyle == o.entryTextStyle;
}
@override
int get hashCode {
return hashValues(
selectionModelType,
contentBuilder,
position,
outsideJustification,
insideJustification,
showMeasures,
legendDefaultMeasure,
measureFormatter,
secondaryMeasureFormatter,
entryTextStyle);
}
}
/// Flutter specific wrapper on the common Legend for building content.
class _FlutterDatumLegend<D> extends common.DatumLegend<D>
implements BuildableBehavior, TappableLegend {
DatumLegend config;
_FlutterDatumLegend(this.config)
: super(
selectionModelType: config.selectionModelType,
measureFormatter: config.measureFormatter,
secondaryMeasureFormatter: config.secondaryMeasureFormatter,
legendDefaultMeasure: config.legendDefaultMeasure,
) {
super.entryTextStyle = config.entryTextStyle;
}
@override
void updateLegend() {
(chartContext as ChartContainerRenderObject).requestRebuild();
}
@override
common.BehaviorPosition get position => config.position;
@override
common.OutsideJustification get outsideJustification =>
config.outsideJustification;
@override
common.InsideJustification get insideJustification =>
config.insideJustification;
@override
Widget build(BuildContext context) {
final hasSelection =
legendState.legendEntries.any((entry) => entry.isSelected);
// Show measures if [showMeasures] is true and there is a selection or if
// showing measures when there is no selection.
final showMeasures = config.showMeasures &&
(hasSelection ||
legendDefaultMeasure != common.LegendDefaultMeasure.none);
return config.contentBuilder
.build(context, legendState, this, showMeasures: showMeasures);
}
/// TODO: Maybe highlight the pie wedge.
@override
onLegendEntryTapUp(common.LegendEntry detail) {}
}

View File

@@ -0,0 +1,22 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' show LegendEntry, LegendTapHandling;
abstract class TappableLegend<T, D> {
/// Delegates handling of legend entry clicks according to the configured
/// [LegendTapHandling] strategy.
onLegendEntryTapUp(LegendEntry detail);
}

View File

@@ -0,0 +1,92 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show Legend, LegendState, SeriesLegend;
import 'package:flutter_web/widgets.dart' show BuildContext, hashValues, Widget;
import 'legend.dart';
import 'legend_entry_layout.dart';
import 'legend_layout.dart';
/// Strategy for building a legend content widget.
abstract class LegendContentBuilder {
const LegendContentBuilder();
Widget build(BuildContext context, common.LegendState legendState,
common.Legend legend,
{bool showMeasures});
}
/// Base strategy for building a legend content widget.
///
/// Each legend entry is passed to a [LegendLayout] strategy to create a widget
/// for each legend entry. These widgets are then passed to a
/// [LegendEntryLayout] strategy to create the legend widget.
abstract class BaseLegendContentBuilder implements LegendContentBuilder {
/// Strategy for creating one widget or each legend entry.
LegendEntryLayout get legendEntryLayout;
/// Strategy for creating the legend content widget from a list of widgets.
///
/// This is typically the list of widgets from legend entries.
LegendLayout get legendLayout;
@override
Widget build(BuildContext context, common.LegendState legendState,
common.Legend legend,
{bool showMeasures}) {
final entryWidgets = legendState.legendEntries.map((entry) {
var isHidden = false;
if (legend is common.SeriesLegend) {
isHidden = legend.isSeriesHidden(entry.series.id);
}
return legendEntryLayout.build(
context, entry, legend as TappableLegend, isHidden,
showMeasures: showMeasures);
}).toList();
return legendLayout.build(context, entryWidgets);
}
}
// TODO: Expose settings for tabular layout.
/// Strategy that builds a tabular legend.
///
/// [legendEntryLayout] custom strategy for creating widgets for each legend
/// entry.
/// [legendLayout] custom strategy for creating legend widget from list of
/// widgets that represent a legend entry.
class TabularLegendContentBuilder extends BaseLegendContentBuilder {
final LegendEntryLayout legendEntryLayout;
final LegendLayout legendLayout;
TabularLegendContentBuilder(
{LegendEntryLayout legendEntryLayout, LegendLayout legendLayout})
: this.legendEntryLayout =
legendEntryLayout ?? const SimpleLegendEntryLayout(),
this.legendLayout =
legendLayout ?? new TabularLegendLayout.horizontalFirst();
@override
bool operator ==(Object o) {
return o is TabularLegendContentBuilder &&
legendEntryLayout == o.legendEntryLayout &&
legendLayout == o.legendLayout;
}
@override
int get hashCode => hashValues(legendEntryLayout, legendLayout);
}

View File

@@ -0,0 +1,144 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common;
import 'package:charts_flutter/src/util/color.dart';
import 'package:flutter_web/widgets.dart';
import 'package:flutter_web/material.dart'
show GestureDetector, GestureTapUpCallback, TapUpDetails, Theme;
import '../../symbol_renderer.dart';
import 'legend.dart' show TappableLegend;
/// Strategy for building one widget from one [common.LegendEntry].
abstract class LegendEntryLayout {
Widget build(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden,
{bool showMeasures});
}
/// Builds one legend entry as a row with symbol and label from the series.
///
/// If directionality from the chart context indicates RTL, the symbol is placed
/// to the right of the text instead of the left of the text.
class SimpleLegendEntryLayout implements LegendEntryLayout {
const SimpleLegendEntryLayout();
Widget createSymbol(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden) {
// TODO: Consider allowing scaling the size for the symbol.
// A custom symbol renderer can ignore this size and use their own.
final materialSymbolSize = new Size(12.0, 12.0);
final entryColor = legendEntry.color;
var color = ColorUtil.toDartColor(entryColor);
// Get the SymbolRendererBuilder wrapping a common.SymbolRenderer if needed.
final SymbolRendererBuilder symbolRendererBuilder =
legendEntry.symbolRenderer is SymbolRendererBuilder
? legendEntry.symbolRenderer
: new SymbolRendererCanvas(legendEntry.symbolRenderer);
return new GestureDetector(
child: symbolRendererBuilder.build(
context,
size: materialSymbolSize,
color: color,
enabled: !isHidden,
),
onTapUp: makeTapUpCallback(context, legendEntry, legend));
}
Widget createLabel(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden) {
TextStyle style =
_convertTextStyle(isHidden, context, legendEntry.textStyle);
return new GestureDetector(
child: new Text(legendEntry.label, style: style),
onTapUp: makeTapUpCallback(context, legendEntry, legend));
}
Widget createMeasureValue(BuildContext context,
common.LegendEntry legendEntry, TappableLegend legend, bool isHidden) {
return new GestureDetector(
child: new Text(legendEntry.formattedValue),
onTapUp: makeTapUpCallback(context, legendEntry, legend));
}
@override
Widget build(BuildContext context, common.LegendEntry legendEntry,
TappableLegend legend, bool isHidden,
{bool showMeasures}) {
final rowChildren = <Widget>[];
// TODO: Allow setting to configure the padding.
final padding = new EdgeInsets.only(right: 8.0); // Material default.
final symbol = createSymbol(context, legendEntry, legend, isHidden);
final label = createLabel(context, legendEntry, legend, isHidden);
final measure = showMeasures
? createMeasureValue(context, legendEntry, legend, isHidden)
: null;
rowChildren.add(symbol);
rowChildren.add(new Container(padding: padding));
rowChildren.add(label);
if (measure != null) {
rowChildren.add(new Container(padding: padding));
rowChildren.add(measure);
}
// Row automatically reverses the content if Directionality is rtl.
return new Row(children: rowChildren);
}
GestureTapUpCallback makeTapUpCallback(BuildContext context,
common.LegendEntry legendEntry, TappableLegend legend) {
return (TapUpDetails d) {
legend.onLegendEntryTapUp(legendEntry);
};
}
bool operator ==(Object other) => other is SimpleLegendEntryLayout;
int get hashCode {
return this.runtimeType.hashCode;
}
/// Convert the charts common TextStlyeSpec into a standard TextStyle, while
/// reducing the color opacity to 26% if the entry is hidden.
///
/// For non-specified values, override the hidden text color to use the body 1
/// theme, but allow other properties of [Text] to be inherited.
TextStyle _convertTextStyle(
bool isHidden, BuildContext context, common.TextStyleSpec textStyle) {
Color color = textStyle?.color != null
? ColorUtil.toDartColor(textStyle.color)
: null;
if (isHidden) {
// Use a default color for hidden legend entries if none is provided.
color ??= Theme.of(context).textTheme.body1.color;
color = color.withOpacity(0.26);
}
return new TextStyle(
inherit: true,
fontFamily: textStyle?.fontFamily,
fontSize:
textStyle?.fontSize != null ? textStyle.fontSize.toDouble() : null,
color: color);
}
}

View File

@@ -0,0 +1,158 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show min;
import 'package:flutter_web/rendering.dart';
import 'package:flutter_web/widgets.dart';
/// Strategy for building legend from legend entry widgets.
abstract class LegendLayout {
Widget build(BuildContext context, List<Widget> legendEntryWidgets);
}
/// Layout legend entries in tabular format.
class TabularLegendLayout implements LegendLayout {
/// No limit for max rows or max columns.
static const _noLimit = -1;
final bool isHorizontalFirst;
final int desiredMaxRows;
final int desiredMaxColumns;
final EdgeInsets cellPadding;
TabularLegendLayout._internal(
{this.isHorizontalFirst,
this.desiredMaxRows,
this.desiredMaxColumns,
this.cellPadding});
/// Layout horizontally until columns exceed [desiredMaxColumns].
///
/// [desiredMaxColumns] the max columns to use before laying out items in a
/// new row. By default there is no limit. The max columns created is the
/// smaller of desiredMaxColumns and number of legend entries.
///
/// [cellPadding] the [EdgeInsets] for each widget.
factory TabularLegendLayout.horizontalFirst({
int desiredMaxColumns,
EdgeInsets cellPadding,
}) {
return new TabularLegendLayout._internal(
isHorizontalFirst: true,
desiredMaxRows: _noLimit,
desiredMaxColumns: desiredMaxColumns ?? _noLimit,
cellPadding: cellPadding,
);
}
/// Layout vertically, until rows exceed [desiredMaxRows].
///
/// [desiredMaxRows] the max rows to use before layout out items in a new
/// column. By default there is no limit. The max columns created is the
/// smaller of desiredMaxRows and number of legend entries.
///
/// [cellPadding] the [EdgeInsets] for each widget.
factory TabularLegendLayout.verticalFirst({
int desiredMaxRows,
EdgeInsets cellPadding,
}) {
return new TabularLegendLayout._internal(
isHorizontalFirst: false,
desiredMaxRows: desiredMaxRows ?? _noLimit,
desiredMaxColumns: _noLimit,
cellPadding: cellPadding,
);
}
@override
Widget build(BuildContext context, List<Widget> legendEntries) {
final paddedLegendEntries = ((cellPadding == null)
? legendEntries
: legendEntries
.map((entry) => new Padding(padding: cellPadding, child: entry))
.toList());
return isHorizontalFirst
? _buildHorizontalFirst(paddedLegendEntries)
: _buildVerticalFirst(paddedLegendEntries);
}
@override
bool operator ==(o) =>
o is TabularLegendLayout &&
desiredMaxRows == o.desiredMaxRows &&
desiredMaxColumns == o.desiredMaxColumns &&
isHorizontalFirst == o.isHorizontalFirst &&
cellPadding == o.cellPadding;
@override
int get hashCode => hashValues(
desiredMaxRows, desiredMaxColumns, isHorizontalFirst, cellPadding);
Widget _buildHorizontalFirst(List<Widget> legendEntries) {
final maxColumns = (desiredMaxColumns == _noLimit)
? legendEntries.length
: min(legendEntries.length, desiredMaxColumns);
final rows = <TableRow>[];
for (var i = 0; i < legendEntries.length; i += maxColumns) {
rows.add(new TableRow(
children: legendEntries
.sublist(i, min(i + maxColumns, legendEntries.length))
.toList()));
}
return _buildTableFromRows(rows);
}
Widget _buildVerticalFirst(List<Widget> legendEntries) {
final maxRows = (desiredMaxRows == _noLimit)
? legendEntries.length
: min(legendEntries.length, desiredMaxRows);
final rows =
new List.generate(maxRows, (_) => new TableRow(children: <Widget>[]));
for (var i = 0; i < legendEntries.length; i++) {
rows[i % maxRows].children.add(legendEntries[i]);
}
return _buildTableFromRows(rows);
}
Table _buildTableFromRows(List<TableRow> rows) {
final padWidget = new Row();
// Pad rows to the max column count, because each TableRow in a table is
// required to have the same number of children.
final columnCount = rows
.map((r) => r.children.length)
.fold(0, (max, current) => (current > max) ? current : max);
for (var i = 0; i < rows.length; i++) {
final rowChildren = rows[i].children;
final padCount = columnCount - rowChildren.length;
if (padCount > 0) {
rowChildren.addAll(new Iterable.generate(padCount, (_) => padWidget));
}
}
// TODO: Investigate other means of creating the tabular legend
// Sizing the column width using [IntrinsicColumnWidth] is expensive per
// Flutter's documentation, but has to be used if the table is desired to
// have a width that is tight on each column.
return new Table(
children: rows, defaultColumnWidth: new IntrinsicColumnWidth());
}
}

View File

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

View File

@@ -0,0 +1,127 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:collection/collection.dart' show ListEquality;
import 'package:charts_common/common.dart' as common
show
LinePointHighlighter,
LinePointHighlighterFollowLineType,
SelectionModelType,
SymbolRenderer;
import 'package:flutter_web/widgets.dart' show hashValues;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that monitors the specified [SelectionModel] and darkens the
/// color for selected data.
///
/// This is typically used for bars and pies to highlight segments.
///
/// It is used in combination with SelectNearest to update the selection model
/// and expand selection out to the domain value.
@immutable
class LinePointHighlighter extends ChartBehavior<common.LinePointHighlighter> {
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
/// Default radius of the dots if the series has no radius mapping function.
///
/// When no radius mapping function is provided, this value will be used as
/// is. [radiusPaddingPx] will not be added to [defaultRadiusPx].
final double defaultRadiusPx;
/// Additional radius value added to the radius of the selected data.
///
/// This value is only used when the series has a radius mapping function
/// defined.
final double radiusPaddingPx;
final common.LinePointHighlighterFollowLineType showHorizontalFollowLine;
final common.LinePointHighlighterFollowLineType showVerticalFollowLine;
/// The dash pattern to be used for drawing the line.
///
/// To disable dash pattern (to draw a solid line), pass in an empty list.
/// This is because if dashPattern is null or not set, it defaults to [1,3].
final List<int> dashPattern;
/// Whether or not follow lines should be drawn across the entire chart draw
/// area, or just from the axis to the point.
///
/// When disabled, measure follow lines will be drawn from the primary measure
/// axis to the point. In RTL mode, this means from the right-hand axis. In
/// LTR mode, from the left-hand axis.
final bool drawFollowLinesAcrossChart;
/// Renderer used to draw the highlighted points.
final common.SymbolRenderer symbolRenderer;
LinePointHighlighter(
{this.selectionModelType,
this.defaultRadiusPx,
this.radiusPaddingPx,
this.showHorizontalFollowLine,
this.showVerticalFollowLine,
this.dashPattern,
this.drawFollowLinesAcrossChart,
this.symbolRenderer});
@override
common.LinePointHighlighter<D> createCommonBehavior<D>() =>
new common.LinePointHighlighter<D>(
selectionModelType: selectionModelType,
defaultRadiusPx: defaultRadiusPx,
radiusPaddingPx: radiusPaddingPx,
showHorizontalFollowLine: showHorizontalFollowLine,
showVerticalFollowLine: showVerticalFollowLine,
dashPattern: dashPattern,
drawFollowLinesAcrossChart: drawFollowLinesAcrossChart,
symbolRenderer: symbolRenderer,
);
@override
void updateCommonBehavior(common.LinePointHighlighter commonBehavior) {}
@override
String get role => 'LinePointHighlighter-${selectionModelType.toString()}';
@override
bool operator ==(Object o) {
return o is LinePointHighlighter &&
defaultRadiusPx == o.defaultRadiusPx &&
radiusPaddingPx == o.radiusPaddingPx &&
showHorizontalFollowLine == o.showHorizontalFollowLine &&
showVerticalFollowLine == o.showVerticalFollowLine &&
selectionModelType == o.selectionModelType &&
new ListEquality().equals(dashPattern, o.dashPattern) &&
drawFollowLinesAcrossChart == o.drawFollowLinesAcrossChart;
}
@override
int get hashCode {
return hashValues(
selectionModelType,
defaultRadiusPx,
radiusPaddingPx,
showHorizontalFollowLine,
showVerticalFollowLine,
dashPattern,
drawFollowLinesAcrossChart,
);
}
}

View File

@@ -0,0 +1,117 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show
AnnotationLabelAnchor,
AnnotationLabelDirection,
AnnotationLabelPosition,
AnnotationSegment,
Color,
MaterialPalette,
RangeAnnotation,
TextStyleSpec;
import 'package:collection/collection.dart' show ListEquality;
import 'package:flutter_web/widgets.dart' show hashValues;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that annotations domain ranges with a solid fill color.
///
/// The annotations will be drawn underneath series data and chart axes.
///
/// This is typically used for line charts to call out sections of the data
/// range.
@immutable
class RangeAnnotation extends ChartBehavior<common.RangeAnnotation> {
final desiredGestures = new Set<GestureType>();
/// List of annotations to render on the chart.
final List<common.AnnotationSegment> annotations;
/// Configures where to anchor annotation label text.
final common.AnnotationLabelAnchor defaultLabelAnchor;
/// Direction of label text on the annotations.
final common.AnnotationLabelDirection defaultLabelDirection;
/// Configures where to place labels relative to the annotation.
final common.AnnotationLabelPosition defaultLabelPosition;
/// Configures the style of label text.
final common.TextStyleSpec defaultLabelStyleSpec;
/// Default color for annotations.
final common.Color defaultColor;
/// Whether or not the range of the axis should be extended to include the
/// annotation start and end values.
final bool extendAxis;
/// Space before and after label text.
final int labelPadding;
RangeAnnotation(this.annotations,
{common.Color defaultColor,
this.defaultLabelAnchor,
this.defaultLabelDirection,
this.defaultLabelPosition,
this.defaultLabelStyleSpec,
this.extendAxis,
this.labelPadding})
: defaultColor = common.MaterialPalette.gray.shade100;
@override
common.RangeAnnotation<D> createCommonBehavior<D>() =>
new common.RangeAnnotation<D>(annotations,
defaultColor: defaultColor,
defaultLabelAnchor: defaultLabelAnchor,
defaultLabelDirection: defaultLabelDirection,
defaultLabelPosition: defaultLabelPosition,
defaultLabelStyleSpec: defaultLabelStyleSpec,
extendAxis: extendAxis,
labelPadding: labelPadding);
@override
void updateCommonBehavior(common.RangeAnnotation commonBehavior) {}
@override
String get role => 'RangeAnnotation';
@override
bool operator ==(Object o) {
return o is RangeAnnotation &&
new ListEquality().equals(annotations, o.annotations) &&
defaultColor == o.defaultColor &&
extendAxis == o.extendAxis &&
defaultLabelAnchor == o.defaultLabelAnchor &&
defaultLabelDirection == o.defaultLabelDirection &&
defaultLabelPosition == o.defaultLabelPosition &&
defaultLabelStyleSpec == o.defaultLabelStyleSpec &&
labelPadding == o.labelPadding;
}
@override
int get hashCode => hashValues(
annotations,
defaultColor,
extendAxis,
defaultLabelAnchor,
defaultLabelDirection,
defaultLabelPosition,
defaultLabelStyleSpec,
labelPadding);
}

View File

@@ -0,0 +1,147 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show ChartBehavior, SelectNearest, SelectionModelType, SelectionTrigger;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that listens to the given eventTrigger and updates the
/// specified [SelectionModel]. This is used to pair input events to behaviors
/// that listen to selection changes.
///
/// Input event types:
/// hover (default) - Mouse over/near data.
/// tap - Mouse/Touch on/near data.
/// pressHold - Mouse/Touch and drag across the data instead of panning.
/// longPressHold - Mouse/Touch for a while in one place then drag across the data.
///
/// SelectionModels that can be updated:
/// info - To view the details of the selected items (ie: hover for web).
/// action - To select an item as an input, drill, or other selection.
///
/// Other options available
/// expandToDomain - all data points that match the domain value of the
/// closest data point will be included in the selection. (Default: true)
/// selectClosestSeries - mark the series for the closest data point as
/// selected. (Default: true)
///
/// You can add one SelectNearest for each model type that you are updating.
/// Any previous SelectNearest behavior for that selection model will be
/// removed.
@immutable
class SelectNearest extends ChartBehavior<common.SelectNearest> {
final Set<GestureType> desiredGestures;
final common.SelectionModelType selectionModelType;
final common.SelectionTrigger eventTrigger;
final bool expandToDomain;
final bool selectAcrossAllDrawAreaComponents;
final bool selectClosestSeries;
final int maximumDomainDistancePx;
SelectNearest._internal(
{this.selectionModelType,
this.expandToDomain = true,
this.selectAcrossAllDrawAreaComponents = false,
this.selectClosestSeries = true,
this.eventTrigger,
this.desiredGestures,
this.maximumDomainDistancePx});
factory SelectNearest(
{common.SelectionModelType selectionModelType =
common.SelectionModelType.info,
bool expandToDomain = true,
bool selectAcrossAllDrawAreaComponents = false,
bool selectClosestSeries = true,
common.SelectionTrigger eventTrigger = common.SelectionTrigger.tap,
int maximumDomainDistancePx}) {
return new SelectNearest._internal(
selectionModelType: selectionModelType,
expandToDomain: expandToDomain,
selectAcrossAllDrawAreaComponents: selectAcrossAllDrawAreaComponents,
selectClosestSeries: selectClosestSeries,
eventTrigger: eventTrigger,
desiredGestures: SelectNearest._getDesiredGestures(eventTrigger),
maximumDomainDistancePx: maximumDomainDistancePx);
}
static Set<GestureType> _getDesiredGestures(
common.SelectionTrigger eventTrigger) {
final desiredGestures = new Set<GestureType>();
switch (eventTrigger) {
case common.SelectionTrigger.tap:
desiredGestures..add(GestureType.onTap);
break;
case common.SelectionTrigger.tapAndDrag:
desiredGestures..add(GestureType.onTap)..add(GestureType.onDrag);
break;
case common.SelectionTrigger.pressHold:
case common.SelectionTrigger.longPressHold:
desiredGestures
..add(GestureType.onTap)
..add(GestureType.onLongPress)
..add(GestureType.onDrag);
break;
case common.SelectionTrigger.hover:
default:
desiredGestures..add(GestureType.onHover);
break;
}
return desiredGestures;
}
@override
common.SelectNearest<D> createCommonBehavior<D>() {
return new common.SelectNearest<D>(
selectionModelType: selectionModelType,
eventTrigger: eventTrigger,
expandToDomain: expandToDomain,
selectClosestSeries: selectClosestSeries,
maximumDomainDistancePx: maximumDomainDistancePx);
}
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {}
// TODO: Explore the performance impact of calculating this once
// at the constructor for this and common ChartBehaviors.
@override
String get role => 'SelectNearest-${selectionModelType.toString()}}';
bool operator ==(Object other) {
if (other is SelectNearest) {
return (selectionModelType == other.selectionModelType) &&
(eventTrigger == other.eventTrigger) &&
(expandToDomain == other.expandToDomain) &&
(selectClosestSeries == other.selectClosestSeries) &&
(maximumDomainDistancePx == other.maximumDomainDistancePx);
} else {
return false;
}
}
int get hashCode {
int hashcode = selectionModelType.hashCode;
hashcode = hashcode * 37 + eventTrigger.hashCode;
hashcode = hashcode * 37 + expandToDomain.hashCode;
hashcode = hashcode * 37 + selectClosestSeries.hashCode;
hashcode = hashcode * 37 + maximumDomainDistancePx.hashCode;
return hashcode;
}
}

View File

@@ -0,0 +1,196 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:charts_common/common.dart' as common
show
LayoutViewPaintOrder,
RectSymbolRenderer,
SelectionTrigger,
Slider,
SliderListenerCallback,
SliderStyle,
SymbolRenderer;
import 'package:flutter_web/widgets.dart' show hashValues;
import 'package:meta/meta.dart' show immutable;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that adds a slider widget to a chart. When the slider is
/// dropped after drag, it will report its domain position and nearest datum
/// value. This behavior only supports charts that use continuous scales.
///
/// Input event types:
/// tapAndDrag - Mouse/Touch on the handle and drag across the chart.
/// pressHold - Mouse/Touch on the handle and drag across the chart instead of
/// panning.
/// longPressHold - Mouse/Touch for a while on the handle, then drag across
/// the data.
@immutable
class Slider extends ChartBehavior<common.Slider> {
final Set<GestureType> desiredGestures;
/// Type of input event for the slider.
///
/// Input event types:
/// tapAndDrag - Mouse/Touch on the handle and drag across the chart.
/// pressHold - Mouse/Touch on the handle and drag across the chart instead
/// of panning.
/// longPressHold - Mouse/Touch for a while on the handle, then drag across
/// the data.
final common.SelectionTrigger eventTrigger;
/// The order to paint slider on the canvas.
///
/// The smaller number is drawn first. This value should be relative to
/// LayoutPaintViewOrder.slider (e.g. LayoutViewPaintOrder.slider + 1).
final int layoutPaintOrder;
/// Initial domain position of the slider, in domain units.
final dynamic initialDomainValue;
/// Callback function that will be called when the position of the slider
/// changes during a drag event.
///
/// The callback will be given the current domain position of the slider.
final common.SliderListenerCallback onChangeCallback;
/// Custom role ID for this slider
final String roleId;
/// Whether or not the slider will snap onto the nearest datum (by domain
/// distance) when dragged.
final bool snapToDatum;
/// Color and size styles for the slider.
final common.SliderStyle style;
/// Renderer for the handle. Defaults to a rectangle.
final common.SymbolRenderer handleRenderer;
Slider._internal(
{this.eventTrigger,
this.onChangeCallback,
this.initialDomainValue,
this.roleId,
this.snapToDatum,
this.style,
this.handleRenderer,
this.desiredGestures,
this.layoutPaintOrder});
/// Constructs a [Slider].
///
/// [eventTrigger] sets the type of gesture handled by the slider.
///
/// [handleRenderer] draws a handle for the slider. Defaults to a rectangle.
///
/// [initialDomainValue] sets the initial position of the slider in domain
/// units. The default is the center of the chart.
///
/// [onChangeCallback] will be called when the position of the slider
/// changes during a drag event.
///
/// [snapToDatum] configures the slider to snap snap onto the nearest datum
/// (by domain distance) when dragged. By default, the slider can be
/// positioned anywhere along the domain axis.
///
/// [style] configures the color and sizing of the slider line and handle.
///
/// [layoutPaintOrder] configures the order in which the behavior should be
/// painted. This value should be relative to LayoutPaintViewOrder.slider.
/// (e.g. LayoutViewPaintOrder.slider + 1).
factory Slider(
{common.SelectionTrigger eventTrigger,
common.SymbolRenderer handleRenderer,
dynamic initialDomainValue,
String roleId,
common.SliderListenerCallback onChangeCallback,
bool snapToDatum = false,
common.SliderStyle style,
int layoutPaintOrder = common.LayoutViewPaintOrder.slider}) {
eventTrigger ??= common.SelectionTrigger.tapAndDrag;
handleRenderer ??= new common.RectSymbolRenderer();
// Default the handle size large enough to tap on a mobile device.
style ??= new common.SliderStyle(handleSize: Rectangle<int>(0, 0, 20, 30));
return new Slider._internal(
eventTrigger: eventTrigger,
handleRenderer: handleRenderer,
initialDomainValue: initialDomainValue,
onChangeCallback: onChangeCallback,
roleId: roleId,
snapToDatum: snapToDatum,
style: style,
desiredGestures: Slider._getDesiredGestures(eventTrigger),
layoutPaintOrder: layoutPaintOrder);
}
static Set<GestureType> _getDesiredGestures(
common.SelectionTrigger eventTrigger) {
final desiredGestures = new Set<GestureType>();
switch (eventTrigger) {
case common.SelectionTrigger.tapAndDrag:
desiredGestures..add(GestureType.onTap)..add(GestureType.onDrag);
break;
case common.SelectionTrigger.pressHold:
case common.SelectionTrigger.longPressHold:
desiredGestures
..add(GestureType.onTap)
..add(GestureType.onLongPress)
..add(GestureType.onDrag);
break;
default:
throw new ArgumentError(
'Slider does not support the event trigger ' + '"${eventTrigger}"');
break;
}
return desiredGestures;
}
@override
common.Slider<D> createCommonBehavior<D>() => new common.Slider<D>(
eventTrigger: eventTrigger,
handleRenderer: handleRenderer,
initialDomainValue: initialDomainValue as D,
onChangeCallback: onChangeCallback,
roleId: roleId,
snapToDatum: snapToDatum,
style: style);
@override
void updateCommonBehavior(common.Slider commonBehavior) {}
@override
String get role => 'Slider-${eventTrigger.toString()}';
@override
bool operator ==(Object o) {
return o is Slider &&
eventTrigger == o.eventTrigger &&
handleRenderer == o.handleRenderer &&
initialDomainValue == o.initialDomainValue &&
onChangeCallback == o.onChangeCallback &&
roleId == o.roleId &&
snapToDatum == o.snapToDatum &&
style == o.style &&
layoutPaintOrder == o.layoutPaintOrder;
}
@override
int get hashCode {
return hashValues(eventTrigger, handleRenderer, initialDomainValue, roleId,
snapToDatum, style, layoutPaintOrder);
}
}

View File

@@ -0,0 +1,53 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show SelectionModelType, SlidingViewport;
import 'package:meta/meta.dart' show immutable;
import 'chart_behavior.dart' show ChartBehavior, GestureType;
/// Chart behavior that centers the viewport on the selected domain.
///
/// It is used in combination with SelectNearest to update the selection model
/// and notify this behavior to update the viewport on selection change.
///
/// This behavior can only be used on [CartesianChart].
@immutable
class SlidingViewport extends ChartBehavior<common.SlidingViewport> {
final desiredGestures = new Set<GestureType>();
final common.SelectionModelType selectionModelType;
SlidingViewport([this.selectionModelType = common.SelectionModelType.info]);
@override
common.SlidingViewport<D> createCommonBehavior<D>() =>
new common.SlidingViewport<D>(selectionModelType);
@override
void updateCommonBehavior(common.SlidingViewport commonBehavior) {}
@override
String get role => 'slidingViewport-${selectionModelType.toString()}';
@override
bool operator ==(Object o) =>
o is SlidingViewport && selectionModelType == o.selectionModelType;
@override
int get hashCode => selectionModelType.hashCode;
}

View File

@@ -0,0 +1,131 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter_web/widgets.dart' show AnimationController;
import 'package:charts_common/common.dart' as common
show BaseChart, ChartBehavior, InitialHintBehavior;
import 'package:meta/meta.dart' show immutable;
import '../../base_chart_state.dart' show BaseChartState;
import '../chart_behavior.dart'
show ChartBehavior, ChartStateBehavior, GestureType;
@immutable
class InitialHintBehavior extends ChartBehavior<common.InitialHintBehavior> {
final desiredGestures = new Set<GestureType>();
final Duration hintDuration;
final double maxHintTranslate;
final double maxHintScaleFactor;
InitialHintBehavior(
{this.hintDuration, this.maxHintTranslate, this.maxHintScaleFactor});
@override
common.InitialHintBehavior<D> createCommonBehavior<D>() {
final behavior = new FlutterInitialHintBehavior<D>();
if (hintDuration != null) {
behavior.hintDuration = hintDuration;
}
if (maxHintTranslate != null) {
behavior.maxHintTranslate = maxHintTranslate;
}
if (maxHintScaleFactor != null) {
behavior.maxHintScaleFactor = maxHintScaleFactor;
}
return behavior;
}
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {}
@override
String get role => 'InitialHint';
bool operator ==(Object other) {
return other is InitialHintBehavior && other.hintDuration == hintDuration;
}
int get hashCode {
return hintDuration.hashCode;
}
}
/// Adds a native animation controller required for [common.InitialHintBehavior]
/// to function.
class FlutterInitialHintBehavior<D> extends common.InitialHintBehavior<D>
implements ChartStateBehavior {
AnimationController _hintAnimator;
BaseChartState _chartState;
set chartState(BaseChartState chartState) {
assert(chartState != null);
_chartState = chartState;
_hintAnimator = _chartState.getAnimationController(this);
_hintAnimator?.addListener(onHintTick);
}
@override
void startHintAnimation() {
super.startHintAnimation();
_hintAnimator
..duration = hintDuration
..forward(from: 0.0);
}
@override
void stopHintAnimation() {
super.stopHintAnimation();
_hintAnimator?.stop();
// Hint animation occurs only on the first draw. The hint animator is no
// longer needed after the hint animation stops and is removed.
_chartState.disposeAnimationController(this);
_hintAnimator = null;
}
@override
double get hintAnimationPercent => _hintAnimator.value;
bool _skippedFirstTick = true;
@override
void onHintTick() {
// Skip the first tick on Flutter because the widget rebuild scheduled
// during onAnimation fails on an assert on render object in the framework.
if (_skippedFirstTick) {
_skippedFirstTick = false;
return;
}
super.onHintTick();
}
@override
removeFrom(common.BaseChart<D> chart) {
_chartState.disposeAnimationController(this);
_hintAnimator = null;
super.removeFrom(chart);
}
}

View File

@@ -0,0 +1,64 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show ChartBehavior, PanAndZoomBehavior, PanningCompletedCallback;
import 'package:meta/meta.dart' show immutable;
import '../chart_behavior.dart' show ChartBehavior, GestureType;
import 'pan_behavior.dart' show FlutterPanBehaviorMixin;
@immutable
class PanAndZoomBehavior extends ChartBehavior<common.PanAndZoomBehavior> {
final _desiredGestures = new Set<GestureType>.from([
GestureType.onDrag,
]);
Set<GestureType> get desiredGestures => _desiredGestures;
/// Optional callback that is called when pan / zoom is completed.
///
/// When flinging this callback is called after the fling is completed.
/// This is because panning is only completed when the flinging stops.
final common.PanningCompletedCallback panningCompletedCallback;
PanAndZoomBehavior({this.panningCompletedCallback});
@override
common.PanAndZoomBehavior<D> createCommonBehavior<D>() {
return new FlutterPanAndZoomBehavior<D>()
..panningCompletedCallback = panningCompletedCallback;
}
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {}
@override
String get role => 'PanAndZoom';
bool operator ==(Object other) {
return other is PanAndZoomBehavior &&
other.panningCompletedCallback == panningCompletedCallback;
}
int get hashCode {
return panningCompletedCallback.hashCode;
}
}
/// Adds fling gesture support to [common.PanAndZoomBehavior], by way of
/// [FlutterPanBehaviorMixin].
class FlutterPanAndZoomBehavior<D> extends common.PanAndZoomBehavior<D>
with FlutterPanBehaviorMixin {}

View File

@@ -0,0 +1,186 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show max, pow, Point;
import 'package:flutter_web_ui/ui.dart' hide Point;
import 'package:flutter_web/widgets.dart' show AnimationController;
import 'package:charts_common/common.dart' as common
show BaseChart, ChartBehavior, PanBehavior, PanningCompletedCallback;
import 'package:meta/meta.dart' show immutable;
import '../../base_chart_state.dart' show BaseChartState;
import '../chart_behavior.dart'
show ChartBehavior, ChartStateBehavior, GestureType;
@immutable
class PanBehavior extends ChartBehavior<common.PanBehavior> {
final _desiredGestures = new Set<GestureType>.from([
GestureType.onDrag,
]);
/// Optional callback that is called when panning is completed.
///
/// When flinging this callback is called after the fling is completed.
/// This is because panning is only completed when the flinging stops.
final common.PanningCompletedCallback panningCompletedCallback;
PanBehavior({this.panningCompletedCallback});
Set<GestureType> get desiredGestures => _desiredGestures;
@override
common.PanBehavior<D> createCommonBehavior<D>() {
return new FlutterPanBehavior<D>()
..panningCompletedCallback = panningCompletedCallback;
}
@override
void updateCommonBehavior(common.ChartBehavior commonBehavior) {}
@override
String get role => 'Pan';
bool operator ==(Object other) {
return other is PanBehavior &&
other.panningCompletedCallback == panningCompletedCallback;
}
int get hashCode {
return panningCompletedCallback.hashCode;
}
}
/// Class extending [common.PanBehavior] with fling gesture support.
class FlutterPanBehavior<D> = common.PanBehavior<D>
with FlutterPanBehaviorMixin;
/// Mixin that adds fling gesture support to [common.PanBehavior] or subclasses
/// thereof.
mixin FlutterPanBehaviorMixin<D> on common.PanBehavior<D>
implements ChartStateBehavior {
BaseChartState _chartState;
set chartState(BaseChartState chartState) {
assert(chartState != null);
_chartState = chartState;
_flingAnimator = _chartState.getAnimationController(this);
_flingAnimator?.addListener(_onFlingTick);
}
AnimationController _flingAnimator;
double _flingAnimationInitialTranslatePx;
double _flingAnimationTargetTranslatePx;
bool _isFlinging = false;
static const flingDistanceMultiplier = 0.15;
static const flingDeceleratorFactor = 1.0;
static const flingDurationMultiplier = 0.15;
static const minimumFlingVelocity = 300.0;
@override
removeFrom(common.BaseChart<D> chart) {
stopFlingAnimation();
_chartState.disposeAnimationController(this);
_flingAnimator = null;
super.removeFrom(chart);
}
@override
bool onTapTest(Point<double> chartPoint) {
super.onTapTest(chartPoint);
stopFlingAnimation();
return true;
}
@override
bool onDragEnd(
Point<double> localPosition, double scale, double pixelsPerSec) {
if (isPanning) {
// Ignore slow drag gestures to avoid jitter.
if (pixelsPerSec.abs() < minimumFlingVelocity) {
onPanEnd();
return true;
}
_startFling(pixelsPerSec);
}
return super.onDragEnd(localPosition, scale, pixelsPerSec);
}
/// Starts a 'fling' in the direction and speed given by [pixelsPerSec].
void _startFling(double pixelsPerSec) {
final domainAxis = chart.domainAxis;
_flingAnimationInitialTranslatePx = domainAxis.viewportTranslatePx;
_flingAnimationTargetTranslatePx = _flingAnimationInitialTranslatePx +
pixelsPerSec * flingDistanceMultiplier;
final flingDuration = new Duration(
milliseconds:
max(200, (pixelsPerSec * flingDurationMultiplier).abs().round()));
_flingAnimator
..duration = flingDuration
..forward(from: 0.0);
_isFlinging = true;
}
/// Decelerates a fling event.
double _decelerate(double value) => flingDeceleratorFactor == 1.0
? 1.0 - (1.0 - value) * (1.0 - value)
: 1.0 - pow(1.0 - value, 2 * flingDeceleratorFactor);
/// Updates the chart axis state on each tick of the [AnimationController].
void _onFlingTick() {
if (!_isFlinging) {
return;
}
final percent = _flingAnimator.value;
final deceleratedPercent = _decelerate(percent);
final translation = lerpDouble(_flingAnimationInitialTranslatePx,
_flingAnimationTargetTranslatePx, deceleratedPercent);
final domainAxis = chart.domainAxis;
domainAxis.setViewportSettings(
domainAxis.viewportScalingFactor, translation,
drawAreaWidth: chart.drawAreaBounds.width);
if (percent >= 1.0) {
stopFlingAnimation();
onPanEnd();
chart.redraw();
} else {
chart.redraw(skipAnimation: true, skipLayout: true);
}
}
/// Stops any current fling animations that may be executing.
void stopFlingAnimation() {
if (_isFlinging) {
_isFlinging = false;
_flingAnimator?.stop();
}
}
}

View File

@@ -0,0 +1,104 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show cos, pi, sin, Point;
import 'package:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show Color;
/// Draws a sector of a circle, with an optional hole in the center.
class CircleSectorPainter {
/// Draws a sector of a circle, with an optional hole in the center.
///
/// [center] The x, y coordinates of the circle's center.
/// [radius] The radius of the circle.
/// [innerRadius] Optional radius of a hole in the center of the circle that
/// should not be filled in as part of the sector.
/// [startAngle] The angle at which the arc starts, measured clockwise from
/// the positive x axis and expressed in radians.
/// [endAngle] The angle at which the arc ends, measured clockwise from the
/// positive x axis and expressed in radians.
/// [fill] Fill color for the sector.
/// [stroke] Stroke color of the arc and radius lines.
/// [strokeWidthPx] Stroke width of the arc and radius lines.
void draw(
{Canvas canvas,
Paint paint,
Point center,
double radius,
double innerRadius,
double startAngle,
double endAngle,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b);
paint.style = PaintingStyle.fill;
final innerRadiusStartPoint = new Point<double>(
innerRadius * cos(startAngle) + center.x,
innerRadius * sin(startAngle) + center.y);
final innerRadiusEndPoint = new Point<double>(
innerRadius * cos(endAngle) + center.x,
innerRadius * sin(endAngle) + center.y);
final radiusStartPoint = new Point<double>(
radius * cos(startAngle) + center.x,
radius * sin(startAngle) + center.y);
final centerOffset = new Offset(center.x, center.y);
final isFullCircle = startAngle != null &&
endAngle != null &&
endAngle - startAngle == 2 * pi;
final midpointAngle = (endAngle + startAngle) / 2;
final path = new Path()
..moveTo(innerRadiusStartPoint.x, innerRadiusStartPoint.y);
path.lineTo(radiusStartPoint.x, radiusStartPoint.y);
// For full circles, draw the arc in two parts.
if (isFullCircle) {
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius),
startAngle, midpointAngle - startAngle, true);
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius),
midpointAngle, endAngle - midpointAngle, true);
} else {
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius),
startAngle, endAngle - startAngle, true);
}
path.lineTo(innerRadiusEndPoint.x, innerRadiusEndPoint.y);
// For full circles, draw the arc in two parts.
if (isFullCircle) {
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius),
endAngle, midpointAngle - endAngle, true);
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius),
midpointAngle, startAngle - midpointAngle, true);
} else {
path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius),
endAngle, startAngle - endAngle, true);
}
// Drawing two copies of this line segment, before and after the arcs,
// ensures that the path actually gets closed correctly.
path.lineTo(radiusStartPoint.x, radiusStartPoint.y);
canvas.drawPath(path, paint);
}
}

View File

@@ -0,0 +1,242 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter_web_ui/ui.dart' as ui show Shader;
import 'dart:math' show Point, Rectangle;
import 'package:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show Color;
/// Draws a simple line.
///
/// Lines may be styled with dash patterns similar to stroke-dasharray in SVG
/// path elements. Dash patterns are currently only supported between vertical
/// or horizontal line segments at this time.
class LinePainter {
/// Draws a simple line.
///
/// [dashPattern] controls the pattern of dashes and gaps in a line. It is a
/// list of lengths of alternating dashes and gaps. The rendering is similar
/// to stroke-dasharray in SVG path elements. An odd number of values in the
/// pattern will be repeated to derive an even number of values. "1,2,3" is
/// equivalent to "1,2,3,1,2,3."
void draw(
{Canvas canvas,
Paint paint,
List<Point> points,
Rectangle<num> clipBounds,
common.Color fill,
common.Color stroke,
bool roundEndCaps,
double strokeWidthPx,
List<int> dashPattern,
ui.Shader shader}) {
if (points.isEmpty) {
return;
}
// Apply clip bounds as a clip region.
if (clipBounds != null) {
canvas
..save()
..clipRect(new Rect.fromLTWH(
clipBounds.left.toDouble(),
clipBounds.top.toDouble(),
clipBounds.width.toDouble(),
clipBounds.height.toDouble()));
}
paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b);
if (shader != null) {
paint.shader = shader;
}
// If the line has a single point, draw a circle.
if (points.length == 1) {
final point = points.first;
paint.style = PaintingStyle.fill;
canvas.drawCircle(new Offset(point.x, point.y), strokeWidthPx, paint);
} else {
if (strokeWidthPx != null) {
paint.strokeWidth = strokeWidthPx;
}
paint.strokeJoin = StrokeJoin.round;
paint.style = PaintingStyle.stroke;
if (dashPattern == null || dashPattern.isEmpty) {
if (roundEndCaps == true) {
paint.strokeCap = StrokeCap.round;
}
_drawSolidLine(canvas, paint, points);
} else {
_drawDashedLine(canvas, paint, points, dashPattern);
}
}
if (clipBounds != null) {
canvas.restore();
}
}
/// Draws solid lines between each point.
void _drawSolidLine(Canvas canvas, Paint paint, List<Point> points) {
// TODO: Extract a native line component which constructs the
// appropriate underlying data structures to avoid conversion.
final path = new Path()
..moveTo(points.first.x.toDouble(), points.first.y.toDouble());
for (var point in points) {
path.lineTo(point.x.toDouble(), point.y.toDouble());
}
canvas.drawPath(path, paint);
}
/// Draws dashed lines lines between each point.
void _drawDashedLine(
Canvas canvas, Paint paint, List<Point> points, List<int> dashPattern) {
final localDashPattern = new List.from(dashPattern);
// If an odd number of parts are defined, repeat the pattern to get an even
// number.
if (dashPattern.length % 2 == 1) {
localDashPattern.addAll(dashPattern);
}
// Stores the previous point in the series.
var previousSeriesPoint = _getOffset(points.first);
var remainder = 0;
var solid = true;
var dashPatternIndex = 0;
// Gets the next segment in the dash pattern, looping back to the
// beginning once the end has been reached.
var getNextDashPatternSegment = () {
final dashSegment = localDashPattern[dashPatternIndex];
dashPatternIndex = (dashPatternIndex + 1) % localDashPattern.length;
return dashSegment;
};
// Array of points that is used to draw a connecting path when only a
// partial dash pattern segment can be drawn in the remaining length of a
// line segment (between two defined points in the shape).
var remainderPoints;
// Draw the path through all the rest of the points in the series.
for (var pointIndex = 1; pointIndex < points.length; pointIndex++) {
// Stores the current point in the series.
final seriesPoint = _getOffset(points[pointIndex]);
if (previousSeriesPoint == seriesPoint) {
// Bypass dash pattern handling if the points are the same.
} else {
// Stores the previous point along the current series line segment where
// we rendered a dash (or left a gap).
var previousPoint = previousSeriesPoint;
var d = _getOffsetDistance(previousSeriesPoint, seriesPoint);
while (d > 0) {
var dashSegment =
remainder > 0 ? remainder : getNextDashPatternSegment();
remainder = 0;
// Create a unit vector in the direction from previous to next point.
final v = seriesPoint - previousPoint;
final u = new Offset(v.dx / v.distance, v.dy / v.distance);
// If the remaining distance is less than the length of the dash
// pattern segment, then cut off the pattern segment for this portion
// of the overall line.
final distance = d < dashSegment ? d : dashSegment.toDouble();
// Compute a vector representing the length of dash pattern segment to
// be drawn.
final nextPoint = previousPoint + (u * distance);
// If we are in a solid portion of the dash pattern, draw a line.
// Else, move on.
if (solid) {
if (remainderPoints != null) {
// If we had a partial un-drawn dash from the previous point along
// the line, draw a path that includes it and the end of the dash
// pattern segment in the current line segment.
remainderPoints.add(new Offset(nextPoint.dx, nextPoint.dy));
final path = new Path()
..moveTo(remainderPoints.first.dx, remainderPoints.first.dy);
for (var p in remainderPoints) {
path.lineTo(p.dx, p.dy);
}
canvas.drawPath(path, paint);
remainderPoints = null;
} else {
if (d < dashSegment && pointIndex < points.length - 1) {
// If the remaining distance d is too small to fit this dash,
// and we have more points in the line, save off a series of
// remainder points so that we can draw a path segment moving in
// the direction of the next point.
//
// Note that we don't need to save anything off for the "blank"
// portions of the pattern because we still take the remaining
// distance into account before starting the next dash in the
// next line segment.
remainderPoints = [
new Offset(previousPoint.dx, previousPoint.dy),
new Offset(nextPoint.dx, nextPoint.dy)
];
} else {
// Otherwise, draw a simple line segment for this dash.
canvas.drawLine(previousPoint, nextPoint, paint);
}
}
}
solid = !solid;
previousPoint = nextPoint;
d = d - dashSegment;
}
// Save off the remaining distance so that we can continue the dash (or
// gap) into the next line segment.
remainder = -d.round();
// If we have a remaining un-drawn distance for the current dash (or
// gap), revert the last change to "solid" so that we will continue
// either drawing a dash or leaving a gap.
if (remainder > 0) {
solid = !solid;
}
}
previousSeriesPoint = seriesPoint;
}
}
/// Converts a [Point] into an [Offset].
Offset _getOffset(Point point) =>
new Offset(point.x.toDouble(), point.y.toDouble());
/// Computes the distance between two [Offset]s, as if they were [Point]s.
num _getOffsetDistance(Offset o1, Offset o2) {
final p1 = new Point(o1.dx, o1.dy);
final p2 = new Point(o2.dx, o2.dy);
return p1.distanceTo(p2);
}
}

View File

@@ -0,0 +1,88 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show cos, sin, Point;
import 'package:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show CanvasPie;
import 'circle_sector_painter.dart' show CircleSectorPainter;
/// Draws a pie chart, with an optional hole in the center.
class PiePainter {
CircleSectorPainter _circleSectorPainter;
/// Draws a pie chart, with an optional hole in the center.
void draw(Canvas canvas, Paint paint, common.CanvasPie canvasPie) {
_circleSectorPainter ??= new CircleSectorPainter();
final center = canvasPie.center;
final radius = canvasPie.radius;
final innerRadius = canvasPie.innerRadius;
for (var slice in canvasPie.slices) {
_circleSectorPainter.draw(
canvas: canvas,
paint: paint,
center: center,
radius: radius,
innerRadius: innerRadius,
startAngle: slice.startAngle,
endAngle: slice.endAngle,
fill: slice.fill);
}
// Draw stroke lines between pie slices. This is done after the slices are
// drawn to ensure that they appear on top.
if (canvasPie.stroke != null &&
canvasPie.strokeWidthPx != null &&
canvasPie.slices.length > 1) {
paint.color = new Color.fromARGB(canvasPie.stroke.a, canvasPie.stroke.r,
canvasPie.stroke.g, canvasPie.stroke.b);
paint.strokeWidth = canvasPie.strokeWidthPx;
paint.strokeJoin = StrokeJoin.bevel;
paint.style = PaintingStyle.stroke;
final path = new Path();
for (var slice in canvasPie.slices) {
final innerRadiusStartPoint = new Point<double>(
innerRadius * cos(slice.startAngle) + center.x,
innerRadius * sin(slice.startAngle) + center.y);
final innerRadiusEndPoint = new Point<double>(
innerRadius * cos(slice.endAngle) + center.x,
innerRadius * sin(slice.endAngle) + center.y);
final radiusStartPoint = new Point<double>(
radius * cos(slice.startAngle) + center.x,
radius * sin(slice.startAngle) + center.y);
final radiusEndPoint = new Point<double>(
radius * cos(slice.endAngle) + center.x,
radius * sin(slice.endAngle) + center.y);
path.moveTo(innerRadiusStartPoint.x, innerRadiusStartPoint.y);
path.lineTo(radiusStartPoint.x, radiusStartPoint.y);
path.moveTo(innerRadiusEndPoint.x, innerRadiusEndPoint.y);
path.lineTo(radiusEndPoint.x, radiusEndPoint.y);
}
canvas.drawPath(path, paint);
}
}
}

View File

@@ -0,0 +1,56 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point;
import 'package:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show Color;
/// Draws a simple point.
///
/// TODO: Support for more shapes than circles?
class PointPainter {
void draw(
{Canvas canvas,
Paint paint,
Point point,
double radius,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
if (point == null) {
return;
}
if (fill != null) {
paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b);
paint.style = PaintingStyle.fill;
canvas.drawCircle(
new Offset(point.x.toDouble(), point.y.toDouble()), radius, paint);
}
// [Canvas.drawCircle] does not support drawing a circle with both a fill
// and a stroke at this time. Use a separate circle for the stroke.
if (stroke != null && strokeWidthPx != null && strokeWidthPx > 0.0) {
paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b);
paint.strokeWidth = strokeWidthPx;
paint.strokeJoin = StrokeJoin.bevel;
paint.style = PaintingStyle.stroke;
canvas.drawCircle(
new Offset(point.x.toDouble(), point.y.toDouble()), radius, paint);
}
}
}

View File

@@ -0,0 +1,96 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Point, Rectangle;
import 'package:flutter_web/material.dart';
import 'package:charts_common/common.dart' as common show Color;
/// Draws a simple line.
///
/// Lines may be styled with dash patterns similar to stroke-dasharray in SVG
/// path elements. Dash patterns are currently only supported between vertical
/// or horizontal line segments at this time.
class PolygonPainter {
/// Draws a simple line.
///
/// [dashPattern] controls the pattern of dashes and gaps in a line. It is a
/// list of lengths of alternating dashes and gaps. The rendering is similar
/// to stroke-dasharray in SVG path elements. An odd number of values in the
/// pattern will be repeated to derive an even number of values. "1,2,3" is
/// equivalent to "1,2,3,1,2,3."
void draw(
{Canvas canvas,
Paint paint,
List<Point> points,
Rectangle<num> clipBounds,
common.Color fill,
common.Color stroke,
double strokeWidthPx}) {
if (points.isEmpty) {
return;
}
// Apply clip bounds as a clip region.
if (clipBounds != null) {
canvas
..save()
..clipRect(new Rect.fromLTWH(
clipBounds.left.toDouble(),
clipBounds.top.toDouble(),
clipBounds.width.toDouble(),
clipBounds.height.toDouble()));
}
final strokeColor = stroke != null
? new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b)
: null;
final fillColor = fill != null
? new Color.fromARGB(fill.a, fill.r, fill.g, fill.b)
: null;
// If the line has a single point, draw a circle.
if (points.length == 1) {
final point = points.first;
paint.color = fillColor;
paint.style = PaintingStyle.fill;
canvas.drawCircle(new Offset(point.x, point.y), strokeWidthPx, paint);
} else {
if (strokeColor != null && strokeWidthPx != null) {
paint.strokeWidth = strokeWidthPx;
paint.strokeJoin = StrokeJoin.bevel;
paint.style = PaintingStyle.stroke;
}
if (fillColor != null) {
paint.color = fillColor;
paint.style = PaintingStyle.fill;
}
final path = new Path()
..moveTo(points.first.x.toDouble(), points.first.y.toDouble());
for (var point in points) {
path.lineTo(point.x.toDouble(), point.y.toDouble());
}
canvas.drawPath(path, paint);
}
if (clipBounds != null) {
canvas.restore();
}
}
}

View File

@@ -0,0 +1,125 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap;
import 'package:meta/meta.dart' show immutable, protected;
import 'package:charts_common/common.dart' as common
show
AxisSpec,
BaseChart,
CartesianChart,
NumericAxis,
NumericAxisSpec,
RTLSpec,
Series,
SeriesRendererConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'base_chart.dart' show BaseChart, LayoutConfig;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
@immutable
abstract class CartesianChart<D> extends BaseChart<D> {
final common.AxisSpec domainAxis;
final common.AxisSpec primaryMeasureAxis;
final common.AxisSpec secondaryMeasureAxis;
final LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes;
final bool flipVerticalAxis;
CartesianChart(
List<common.Series<dynamic, D>> seriesList, {
bool animate,
Duration animationDuration,
this.domainAxis,
this.primaryMeasureAxis,
this.secondaryMeasureAxis,
this.disjointMeasureAxes,
common.SeriesRendererConfig<D> defaultRenderer,
List<common.SeriesRendererConfig<D>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<D>> selectionModels,
common.RTLSpec rtlSpec,
bool defaultInteractions: true,
LayoutConfig layoutConfig,
UserManagedState userManagedState,
this.flipVerticalAxis,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
defaultInteractions: defaultInteractions,
layoutConfig: layoutConfig,
userManagedState: userManagedState,
);
@override
void updateCommonChart(common.BaseChart baseChart, BaseChart oldWidget,
BaseChartState chartState) {
super.updateCommonChart(baseChart, oldWidget, chartState);
final prev = oldWidget as CartesianChart;
final chart = baseChart as common.CartesianChart;
if (flipVerticalAxis != null) {
chart.flipVerticalAxisOutput = flipVerticalAxis;
}
if (domainAxis != null && domainAxis != prev?.domainAxis) {
chart.domainAxisSpec = domainAxis;
chartState.markChartDirty();
}
if (primaryMeasureAxis != null &&
primaryMeasureAxis != prev?.primaryMeasureAxis) {
chart.primaryMeasureAxisSpec = primaryMeasureAxis;
chartState.markChartDirty();
}
if (secondaryMeasureAxis != null &&
secondaryMeasureAxis != prev?.secondaryMeasureAxis) {
chart.secondaryMeasureAxisSpec = secondaryMeasureAxis;
chartState.markChartDirty();
}
if (disjointMeasureAxes != null &&
disjointMeasureAxes != prev?.disjointMeasureAxes) {
chart.disjointMeasureAxisSpecs = disjointMeasureAxes;
chartState.markChartDirty();
}
}
@protected
LinkedHashMap<String, common.NumericAxis> createDisjointMeasureAxes() {
if (disjointMeasureAxes != null) {
final disjointAxes = new LinkedHashMap<String, common.NumericAxis>();
disjointMeasureAxes
.forEach((String axisId, common.NumericAxisSpec axisSpec) {
disjointAxes[axisId] = axisSpec.createAxis();
});
return disjointAxes;
} else {
return null;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,136 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:async' show Timer;
import 'dart:math' show Point;
import 'package:flutter_web/material.dart'
show
BuildContext,
GestureDetector,
ScaleEndDetails,
ScaleStartDetails,
ScaleUpdateDetails,
TapDownDetails,
TapUpDetails;
import 'behaviors/chart_behavior.dart' show GestureType;
import 'chart_container.dart' show ChartContainer, ChartContainerRenderObject;
import 'util.dart' show getChartContainerRenderObject;
// From https://docs.flutter.io/flutter/gestures/kLongPressTimeout-constant.html
const Duration _kLongPressTimeout = const Duration(milliseconds: 500);
class ChartGestureDetector {
bool _listeningForLongPress;
bool _isDragging = false;
Timer _longPressTimer;
Point<double> _lastTapPoint;
double _lastScale;
_ContainerResolver _containerResolver;
makeWidget(BuildContext context, ChartContainer chartContainer,
Set<GestureType> desiredGestures) {
_containerResolver =
() => getChartContainerRenderObject(context.findRenderObject());
final wantTapDown = desiredGestures.isNotEmpty;
final wantTap = desiredGestures.contains(GestureType.onTap);
final wantDrag = desiredGestures.contains(GestureType.onDrag);
// LongPress is special, we'd like to be able to trigger long press before
// Drag/Press to trigger tooltips then explore with them. This means we
// can't rely on gesture detection since it will block out the scale
// gestures.
_listeningForLongPress = desiredGestures.contains(GestureType.onLongPress);
return new GestureDetector(
child: chartContainer,
onTapDown: wantTapDown ? onTapDown : null,
onTapUp: wantTap ? onTapUp : null,
onScaleStart: wantDrag ? onScaleStart : null,
onScaleUpdate: wantDrag ? onScaleUpdate : null,
onScaleEnd: wantDrag ? onScaleEnd : null,
);
}
void onTapDown(TapDownDetails d) {
final container = _containerResolver();
final localPosition = container.globalToLocal(d.globalPosition);
_lastTapPoint = new Point(localPosition.dx, localPosition.dy);
container.gestureProxy.onTapTest(_lastTapPoint);
// Kick off a timer to see if this is a LongPress.
if (_listeningForLongPress) {
_longPressTimer = new Timer(_kLongPressTimeout, () {
onLongPress();
_longPressTimer = null;
});
}
}
void onTapUp(TapUpDetails d) {
_longPressTimer?.cancel();
final container = _containerResolver();
final localPosition = container.globalToLocal(d.globalPosition);
_lastTapPoint = new Point(localPosition.dx, localPosition.dy);
container.gestureProxy.onTap(_lastTapPoint);
}
void onLongPress() {
final container = _containerResolver();
container.gestureProxy.onLongPress(_lastTapPoint);
}
void onScaleStart(ScaleStartDetails d) {
_longPressTimer?.cancel();
final container = _containerResolver();
final localPosition = container.globalToLocal(d.focalPoint);
_lastTapPoint = new Point(localPosition.dx, localPosition.dy);
_isDragging = container.gestureProxy.onDragStart(_lastTapPoint);
}
void onScaleUpdate(ScaleUpdateDetails d) {
if (!_isDragging) {
return;
}
final container = _containerResolver();
final localPosition = container.globalToLocal(d.focalPoint);
_lastTapPoint = new Point(localPosition.dx, localPosition.dy);
_lastScale = d.scale;
container.gestureProxy.onDragUpdate(_lastTapPoint, d.scale);
}
void onScaleEnd(ScaleEndDetails d) {
if (!_isDragging) {
return;
}
final container = _containerResolver();
container.gestureProxy
.onDragEnd(_lastTapPoint, _lastScale, d.velocity.pixelsPerSecond.dx);
}
}
// Exposed for testing.
typedef ChartContainerRenderObject _ContainerResolver();

View File

@@ -0,0 +1,36 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
abstract class ChartState {
void setAnimation(Duration transition);
/// Request to the native platform to rebuild the chart.
void requestRebuild();
/// Informs the chart that the configuration has changed.
///
/// This flag is set by checks that detect if a configuration has changed,
/// such as behaviors, axis, and renderers.
///
/// This flag is read on chart rebuild, if chart is marked as dirty, then the
/// chart will call a base chart draw.
void markChartDirty();
/// Reset the chart dirty flag.
void resetChartDirtyFlag();
/// Gets if the chart is dirty.
bool get chartIsDirty;
}

View File

@@ -0,0 +1,122 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show
AxisSpec,
NumericCartesianChart,
OrdinalCartesianChart,
RTLSpec,
Series,
SeriesRendererConfig;
import '../behaviors/chart_behavior.dart' show ChartBehavior;
import '../base_chart.dart' show LayoutConfig;
import '../base_chart_state.dart' show BaseChartState;
import '../cartesian_chart.dart' show CartesianChart;
import '../selection_model_config.dart' show SelectionModelConfig;
/// A numeric combo chart supports rendering each series of data with different
/// series renderers.
///
/// Note that if you have DateTime data, you should use [TimeSeriesChart]. We do
/// not expose a separate DateTimeComboChart because it would just be a copy of
/// that chart.
class NumericComboChart extends CartesianChart<num> {
NumericComboChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
common.SeriesRendererConfig<num> defaultRenderer,
List<common.SeriesRendererConfig<num>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<num>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
);
@override
common.NumericCartesianChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.NumericCartesianChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis());
}
}
/// An ordinal combo chart supports rendering each series of data with different
/// series renderers.
class OrdinalComboChart extends CartesianChart<String> {
OrdinalComboChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
common.SeriesRendererConfig<String> defaultRenderer,
List<common.SeriesRendererConfig<String>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<String>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
);
@override
common.OrdinalCartesianChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.OrdinalCartesianChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis());
}
}

View File

@@ -0,0 +1,50 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show GraphicsFactory, LineStyle, TextElement, TextStyle;
import 'package:flutter_web/widgets.dart' show BuildContext, MediaQuery;
import 'line_style.dart' show LineStyle;
import 'text_element.dart' show TextElement;
import 'text_style.dart' show TextStyle;
class GraphicsFactory implements common.GraphicsFactory {
final double textScaleFactor;
GraphicsFactory(BuildContext context,
{GraphicsFactoryHelper helper = const GraphicsFactoryHelper()})
: textScaleFactor = helper.getTextScaleFactorOf(context);
/// Returns a [TextStyle] object.
@override
common.TextStyle createTextPaint() => new TextStyle();
/// Returns a text element from [text] and [style].
@override
common.TextElement createTextElement(String text) {
return new TextElement(text, textScaleFactor: textScaleFactor);
}
@override
common.LineStyle createLinePaint() => new LineStyle();
}
/// Wraps the MediaQuery function to allow for testing.
class GraphicsFactoryHelper {
const GraphicsFactoryHelper();
double getTextScaleFactorOf(BuildContext context) =>
MediaQuery.textScaleFactorOf(context);
}

View File

@@ -0,0 +1,90 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap;
import 'package:charts_common/common.dart' as common
show
AxisSpec,
LineChart,
NumericAxisSpec,
RTLSpec,
Series,
LineRendererConfig,
SeriesRendererConfig;
import 'behaviors/line_point_highlighter.dart' show LinePointHighlighter;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'base_chart.dart' show LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'cartesian_chart.dart' show CartesianChart;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
class LineChart extends CartesianChart<num> {
LineChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes,
common.LineRendererConfig<num> defaultRenderer,
List<common.SeriesRendererConfig<num>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<num>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
bool flipVerticalAxis,
UserManagedState<num> userManagedState,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
flipVerticalAxis: flipVerticalAxis,
userManagedState: userManagedState,
);
@override
common.LineChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.LineChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(),
disjointMeasureAxes: createDisjointMeasureAxes());
}
@override
void addDefaultInteractions(List<ChartBehavior> behaviors) {
super.addDefaultInteractions(behaviors);
behaviors.add(new LinePointHighlighter());
}
}

View File

@@ -0,0 +1,25 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common show Color, LineStyle;
class LineStyle implements common.LineStyle {
@override
common.Color color;
@override
List<int> dashPattern;
@override
int strokeWidth;
}

View File

@@ -0,0 +1,54 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show ArcRendererConfig, PieChart, RTLSpec, Series;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'base_chart.dart' show BaseChart, LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'selection_model_config.dart' show SelectionModelConfig;
class PieChart<D> extends BaseChart<D> {
PieChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.ArcRendererConfig<D> defaultRenderer,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<D>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
defaultRenderer: defaultRenderer,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
);
@override
common.PieChart<D> createCommonChart(BaseChartState chartState) =>
new common.PieChart<D>(layoutConfig: layoutConfig?.commonLayoutConfig);
@override
void addDefaultInteractions(List<ChartBehavior> behaviors) {
super.addDefaultInteractions(behaviors);
}
}

View File

@@ -0,0 +1,82 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap;
import 'package:charts_common/common.dart' as common
show
AxisSpec,
NumericAxisSpec,
PointRendererConfig,
RTLSpec,
ScatterPlotChart,
SeriesRendererConfig,
Series;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'base_chart.dart' show LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'cartesian_chart.dart' show CartesianChart;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
class ScatterPlotChart extends CartesianChart<num> {
ScatterPlotChart(
List<common.Series> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes,
common.PointRendererConfig<num> defaultRenderer,
List<common.SeriesRendererConfig<num>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<num>> selectionModels,
common.RTLSpec rtlSpec,
LayoutConfig layoutConfig,
bool defaultInteractions: true,
bool flipVerticalAxis,
UserManagedState<num> userManagedState,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
rtlSpec: rtlSpec,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
flipVerticalAxis: flipVerticalAxis,
userManagedState: userManagedState,
);
@override
common.ScatterPlotChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.ScatterPlotChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(),
disjointMeasureAxes: createDisjointMeasureAxes());
}
}

View File

@@ -0,0 +1,34 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:meta/meta.dart' show immutable;
import 'package:charts_common/common.dart' as common;
@immutable
class SelectionModelConfig<D> {
final common.SelectionModelType type;
/// Listens for change in selection.
final common.SelectionModelListener<D> changedListener;
/// Listens anytime update selection is called.
final common.SelectionModelListener<D> updatedListener;
SelectionModelConfig(
{this.type = common.SelectionModelType.info,
this.changedListener,
this.updatedListener});
}

View File

@@ -0,0 +1,104 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:math' show Rectangle;
import 'package:charts_common/common.dart' as common
show ChartCanvas, Color, SymbolRenderer;
import 'package:flutter_web/widgets.dart';
import 'chart_canvas.dart' show ChartCanvas;
import 'graphics_factory.dart' show GraphicsFactory;
/// Flutter widget responsible for painting a common SymbolRenderer from the
/// chart.
///
/// If you want to customize the symbol, then use [CustomSymbolRenderer].
class SymbolRendererCanvas implements SymbolRendererBuilder {
final common.SymbolRenderer commonSymbolRenderer;
SymbolRendererCanvas(this.commonSymbolRenderer);
@override
Widget build(BuildContext context,
{Color color, Size size, bool enabled = true}) {
if (!enabled) {
color = color.withOpacity(0.26);
}
return new SizedBox.fromSize(
size: size,
child: new CustomPaint(
painter:
new _SymbolCustomPaint(context, commonSymbolRenderer, color)));
}
}
/// Convenience class allowing you to pass your Widget builder through the
/// common chart so that it is created for you by the Legend.
///
/// This allows a custom SymbolRenderer in Flutter without having to create
/// a completely custom legend.
abstract class CustomSymbolRenderer extends common.SymbolRenderer
implements SymbolRendererBuilder {
/// Must override this method to build the custom Widget with the given color
/// as
@override
Widget build(BuildContext context, {Color color, Size size, bool enabled});
@override
void paint(common.ChartCanvas canvas, Rectangle<num> bounds,
{List<int> dashPattern,
common.Color fillColor,
common.Color strokeColor,
double strokeWidthPx}) {
// Intentionally ignored (never called).
}
@override
bool shouldRepaint(common.SymbolRenderer oldRenderer) {
return false; // Repainting is handled directly in Flutter.
}
}
/// Common interface for [CustomSymbolRenderer] & [SymbolRendererCanvas] for
/// convenience for [LegendEntryLayout].
abstract class SymbolRendererBuilder {
Widget build(BuildContext context, {Color color, Size size, bool enabled});
}
/// The Widget which fulfills the guts of [SymbolRendererCanvas] actually
/// painting the symbol to a canvas using [CustomPainter].
class _SymbolCustomPaint extends CustomPainter {
final BuildContext context;
final common.SymbolRenderer symbolRenderer;
final Color color;
_SymbolCustomPaint(this.context, this.symbolRenderer, this.color);
@override
void paint(Canvas canvas, Size size) {
final bounds =
new Rectangle<num>(0, 0, size.width.toInt(), size.height.toInt());
final commonColor = new common.Color(
r: color.red, g: color.green, b: color.blue, a: color.alpha);
symbolRenderer.paint(
new ChartCanvas(canvas, GraphicsFactory(context)), bounds,
fillColor: commonColor, strokeColor: commonColor);
}
@override
bool shouldRepaint(_SymbolCustomPaint oldDelegate) {
return symbolRenderer.shouldRepaint(oldDelegate.symbolRenderer);
}
}

View File

@@ -0,0 +1,183 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter_web_ui/ui.dart' show TextAlign, TextDirection;
import 'package:charts_common/common.dart' as common
show
MaxWidthStrategy,
TextElement,
TextDirection,
TextMeasurement,
TextStyle;
import 'package:flutter_web/rendering.dart'
show Color, TextBaseline, TextPainter, TextSpan, TextStyle;
/// Flutter implementation for text measurement and painter.
class TextElement implements common.TextElement {
static const ellipsis = '\u{2026}';
@override
final String text;
final double textScaleFactor;
var _painterReady = false;
common.TextStyle _textStyle;
common.TextDirection _textDirection = common.TextDirection.ltr;
int _maxWidth;
common.MaxWidthStrategy _maxWidthStrategy;
TextPainter _textPainter;
common.TextMeasurement _measurement;
double _opacity;
TextElement(this.text, {common.TextStyle style, this.textScaleFactor})
: _textStyle = style;
@override
common.TextStyle get textStyle => _textStyle;
@override
set textStyle(common.TextStyle value) {
if (_textStyle == value) {
return;
}
_textStyle = value;
_painterReady = false;
}
@override
set textDirection(common.TextDirection direction) {
if (_textDirection == direction) {
return;
}
_textDirection = direction;
_painterReady = false;
}
@override
common.TextDirection get textDirection => _textDirection;
@override
int get maxWidth => _maxWidth;
@override
set maxWidth(int value) {
if (_maxWidth == value) {
return;
}
_maxWidth = value;
_painterReady = false;
}
@override
common.MaxWidthStrategy get maxWidthStrategy => _maxWidthStrategy;
@override
set maxWidthStrategy(common.MaxWidthStrategy maxWidthStrategy) {
if (_maxWidthStrategy == maxWidthStrategy) {
return;
}
_maxWidthStrategy = maxWidthStrategy;
_painterReady = false;
}
@override
set opacity(double opacity) {
if (opacity != _opacity) {
_painterReady = false;
_opacity = opacity;
}
}
@override
common.TextMeasurement get measurement {
if (!_painterReady) {
_refreshPainter();
}
return _measurement;
}
/// The estimated distance between where we asked to draw the text (top, left)
/// and where it visually started (top + verticalFontShift, left).
///
/// 10% of reported font height seems to be about right.
int get verticalFontShift {
if (!_painterReady) {
_refreshPainter();
}
return (_textPainter.height * 0.1).ceil();
}
TextPainter get textPainter {
if (!_painterReady) {
_refreshPainter();
}
return _textPainter;
}
/// Create text painter and measure based on current settings
void _refreshPainter() {
_opacity ??= 1.0;
var color = new Color.fromARGB(
(textStyle.color.a * _opacity).round(),
textStyle.color.r,
textStyle.color.g,
textStyle.color.b,
);
_textPainter = new TextPainter(
text: new TextSpan(
text: text,
style: new TextStyle(
color: color,
fontSize: textStyle.fontSize.toDouble(),
fontFamily: textStyle.fontFamily)))
..textDirection = TextDirection.ltr
// TODO Flip once textAlign works
..textAlign = TextAlign.left
// ..textAlign = _textDirection == common.TextDirection.rtl ?
// TextAlign.right : TextAlign.left
..ellipsis = maxWidthStrategy == common.MaxWidthStrategy.ellipsize
? ellipsis
: null;
if (textScaleFactor != null) {
_textPainter.textScaleFactor = textScaleFactor;
}
_textPainter.layout(maxWidth: maxWidth?.toDouble() ?? double.infinity);
final baseline =
_textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
// Estimating the actual draw height to 70% of measures size.
//
// The font reports a size larger than the drawn size, which makes it
// difficult to shift the text around to get it to visually line up
// vertically with other components.
_measurement = new common.TextMeasurement(
horizontalSliceWidth: _textPainter.width,
verticalSliceWidth: _textPainter.height * 0.70,
baseline: baseline);
_painterReady = true;
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter_web_ui/ui.dart' show hashValues;
import 'package:charts_common/common.dart' as common show Color, TextStyle;
class TextStyle implements common.TextStyle {
int fontSize;
String fontFamily;
common.Color color;
@override
bool operator ==(Object other) =>
other is TextStyle &&
fontSize == other.fontSize &&
fontFamily == other.fontFamily &&
color == other.color;
@override
int get hashCode => hashValues(fontSize, fontFamily, color);
}

View File

@@ -0,0 +1,94 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection' show LinkedHashMap;
import 'package:charts_common/common.dart' as common
show
AxisSpec,
DateTimeFactory,
NumericAxisSpec,
Series,
SeriesRendererConfig,
TimeSeriesChart;
import 'behaviors/chart_behavior.dart' show ChartBehavior;
import 'behaviors/line_point_highlighter.dart' show LinePointHighlighter;
import 'cartesian_chart.dart' show CartesianChart;
import 'base_chart.dart' show LayoutConfig;
import 'base_chart_state.dart' show BaseChartState;
import 'selection_model_config.dart' show SelectionModelConfig;
import 'user_managed_state.dart' show UserManagedState;
class TimeSeriesChart extends CartesianChart<DateTime> {
final common.DateTimeFactory dateTimeFactory;
/// Create a [TimeSeriesChart].
///
/// [dateTimeFactory] allows specifying a factory that creates [DateTime] to
/// be used for the time axis. If none specified, local date time is used.
TimeSeriesChart(
List<common.Series<dynamic, DateTime>> seriesList, {
bool animate,
Duration animationDuration,
common.AxisSpec domainAxis,
common.AxisSpec primaryMeasureAxis,
common.AxisSpec secondaryMeasureAxis,
LinkedHashMap<String, common.NumericAxisSpec> disjointMeasureAxes,
common.SeriesRendererConfig<DateTime> defaultRenderer,
List<common.SeriesRendererConfig<DateTime>> customSeriesRenderers,
List<ChartBehavior> behaviors,
List<SelectionModelConfig<DateTime>> selectionModels,
LayoutConfig layoutConfig,
this.dateTimeFactory,
bool defaultInteractions: true,
bool flipVerticalAxis,
UserManagedState<DateTime> userManagedState,
}) : super(
seriesList,
animate: animate,
animationDuration: animationDuration,
domainAxis: domainAxis,
primaryMeasureAxis: primaryMeasureAxis,
secondaryMeasureAxis: secondaryMeasureAxis,
disjointMeasureAxes: disjointMeasureAxes,
defaultRenderer: defaultRenderer,
customSeriesRenderers: customSeriesRenderers,
behaviors: behaviors,
selectionModels: selectionModels,
layoutConfig: layoutConfig,
defaultInteractions: defaultInteractions,
flipVerticalAxis: flipVerticalAxis,
userManagedState: userManagedState,
);
@override
common.TimeSeriesChart createCommonChart(BaseChartState chartState) {
// Optionally create primary and secondary measure axes if the chart was
// configured with them. If no axes were configured, then the chart will
// use its default types (usually a numeric axis).
return new common.TimeSeriesChart(
layoutConfig: layoutConfig?.commonLayoutConfig,
primaryMeasureAxis: primaryMeasureAxis?.createAxis(),
secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(),
disjointMeasureAxes: createDisjointMeasureAxes());
}
@override
void addDefaultInteractions(List<ChartBehavior> behaviors) {
super.addDefaultInteractions(behaviors);
behaviors.add(new LinePointHighlighter());
}
}

View File

@@ -0,0 +1,78 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common
show ImmutableSeries, SelectionModel, SelectionModelType, SeriesDatumConfig;
/// Contains override settings for the internal chart state.
///
/// The chart will check non null settings and apply them if they differ from
/// the internal chart state and trigger the appropriate level of redrawing.
class UserManagedState<D> {
/// The expected selection(s) on the chart.
///
/// If this is set and the model for the selection model type differs from
/// what is in the internal chart state, the selection will be applied and
/// repainting will occur such that behaviors that draw differently on
/// selection change can update, such as the line point highlighter.
///
/// If more than one type of selection model is used, only the one(s)
/// specified in this list will override what is kept in the internally.
///
/// To clear the selection, add an empty selection model.
final selectionModels =
<common.SelectionModelType, UserManagedSelectionModel<D>>{};
}
/// Container for the user managed selection model.
///
/// This container is needed because the selection model generated by selection
/// events is a [SelectionModel], while any user defined selection has to be
/// specified by passing in [selectedSeriesConfig] and [selectedDataConfig].
/// The configuration is converted to a selection model after the series data
/// has been processed.
class UserManagedSelectionModel<D> {
final List<String> selectedSeriesConfig;
final List<common.SeriesDatumConfig> selectedDataConfig;
common.SelectionModel<D> _model;
/// Creates a [UserManagedSelectionModel] that holds [SelectionModel].
///
/// [selectedSeriesConfig] and [selectedDataConfig] is set to null because the
/// [_model] is returned when [getModel] is called.
UserManagedSelectionModel({common.SelectionModel<D> model})
: _model = model ?? new common.SelectionModel(),
selectedSeriesConfig = null,
selectedDataConfig = null;
/// Creates a [UserManagedSelectionModel] with configuration that is converted
/// to a [SelectionModel] when [getModel] provides a processed series list.
UserManagedSelectionModel.fromConfig(
{List<String> selectedSeriesConfig,
List<common.SeriesDatumConfig> selectedDataConfig})
: this.selectedSeriesConfig = selectedSeriesConfig ?? <String>[],
this.selectedDataConfig =
selectedDataConfig ?? <common.SeriesDatumConfig>[];
/// Gets the selection model. If the model is null, create one from
/// configuration and the processed [seriesList] passed in.
common.SelectionModel<D> getModel(
List<common.ImmutableSeries<D>> seriesList) {
_model ??= new common.SelectionModel.fromConfig(
selectedDataConfig, selectedSeriesConfig, seriesList);
return _model;
}
}

View File

@@ -0,0 +1,45 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter_web/rendering.dart'
show
RenderBox,
RenderSemanticsGestureHandler,
RenderPointerListener,
RenderCustomMultiChildLayoutBox;
import 'chart_container.dart' show ChartContainerRenderObject;
/// Get the [ChartContainerRenderObject] from a [RenderBox].
///
/// [RenderBox] is expected to be a [RenderSemanticsGestureHandler] with child
/// of [RenderPointerListener] with child of [ChartContainerRenderObject].
ChartContainerRenderObject getChartContainerRenderObject(RenderBox box) {
assert(box is RenderCustomMultiChildLayoutBox);
final semanticHandler = (box as RenderCustomMultiChildLayoutBox)
.getChildrenAsList()
.firstWhere((child) => child is RenderSemanticsGestureHandler);
assert(semanticHandler is RenderSemanticsGestureHandler);
final renderPointerListener =
(semanticHandler as RenderSemanticsGestureHandler).child;
assert(renderPointerListener is RenderPointerListener);
final chartContainerRenderObject =
(renderPointerListener as RenderPointerListener).child;
assert(chartContainerRenderObject is ChartContainerRenderObject);
return chartContainerRenderObject as ChartContainerRenderObject;
}

View File

@@ -0,0 +1,28 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:charts_common/common.dart' as common show Color;
import 'package:flutter_web_ui/ui.dart' as ui;
class ColorUtil {
static ui.Color toDartColor(common.Color color) {
return ui.Color.fromARGB(color.a, color.r, color.g, color.b);
}
static common.Color fromDartColor(ui.Color color) {
return common.Color(
r: color.red, g: color.green, b: color.blue, a: color.alpha);
}
}

View File

@@ -0,0 +1,219 @@
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
// for details.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:flutter_web_ui/ui.dart' show Offset;
import 'package:flutter_web/material.dart';
import 'package:flutter_web/rendering.dart';
import 'package:flutter_web/widgets.dart';
import 'package:charts_common/common.dart' as common
show BehaviorPosition, InsideJustification, OutsideJustification;
import 'behaviors/chart_behavior.dart' show BuildableBehavior;
/// Layout delegate that layout chart widget with [BuildableBehavior] widgets.
class WidgetLayoutDelegate extends MultiChildLayoutDelegate {
/// ID of the common chart widget.
final String chartID;
/// Directionality of the widget.
final isRTL;
/// ID and [BuildableBehavior] of the widgets for calculating offset.
final Map<String, BuildableBehavior> idAndBehavior;
WidgetLayoutDelegate(this.chartID, this.idAndBehavior, this.isRTL);
@override
void performLayout(Size size) {
// TODO: Change this to a layout manager that supports more
// than one buildable behavior that changes chart size. Remove assert when
// this is possible.
assert(idAndBehavior.keys.isEmpty || idAndBehavior.keys.length == 1);
// Size available for the chart widget.
var availableWidth = size.width;
var availableHeight = size.height;
var chartOffset = Offset.zero;
// Measure the first buildable behavior.
final behaviorID =
idAndBehavior.keys.isNotEmpty ? idAndBehavior.keys.first : null;
var behaviorSize = Size.zero;
if (behaviorID != null) {
if (hasChild(behaviorID)) {
final leftPosition =
isRTL ? common.BehaviorPosition.end : common.BehaviorPosition.start;
final rightPosition =
isRTL ? common.BehaviorPosition.start : common.BehaviorPosition.end;
final behaviorPosition = idAndBehavior[behaviorID].position;
behaviorSize = layoutChild(behaviorID, new BoxConstraints.loose(size));
if (behaviorPosition == common.BehaviorPosition.top) {
chartOffset = new Offset(0.0, behaviorSize.height);
availableHeight -= behaviorSize.height;
} else if (behaviorPosition == common.BehaviorPosition.bottom) {
availableHeight -= behaviorSize.height;
} else if (behaviorPosition == leftPosition) {
chartOffset = new Offset(behaviorSize.width, 0.0);
availableWidth -= behaviorSize.width;
} else if (behaviorPosition == rightPosition) {
availableWidth -= behaviorSize.width;
}
}
}
// Layout chart.
final chartSize = new Size(availableWidth, availableHeight);
if (hasChild(chartID)) {
layoutChild(chartID, new BoxConstraints.tight(chartSize));
positionChild(chartID, chartOffset);
}
// Position buildable behavior.
if (behaviorID != null) {
// TODO: Unable to relayout with new smaller width.
// In the delegate, all children are required to have layout called
// exactly once.
final behaviorOffset = _getBehaviorOffset(idAndBehavior[behaviorID],
behaviorSize: behaviorSize, chartSize: chartSize, isRTL: isRTL);
positionChild(behaviorID, behaviorOffset);
}
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) {
// TODO: Deep equality check because the instance will not be
// the same on each build, even if the buildable behavior has not changed.
return idAndBehavior != (oldDelegate as WidgetLayoutDelegate).idAndBehavior;
}
// Calculate buildable behavior's offset.
Offset _getBehaviorOffset(BuildableBehavior behavior,
{Size behaviorSize, Size chartSize, bool isRTL}) {
Offset behaviorOffset;
final behaviorPosition = behavior.position;
final outsideJustification = behavior.outsideJustification;
final insideJustification = behavior.insideJustification;
if (behaviorPosition == common.BehaviorPosition.top ||
behaviorPosition == common.BehaviorPosition.bottom) {
final heightOffset = behaviorPosition == common.BehaviorPosition.bottom
? chartSize.height
: 0.0;
final horizontalJustification =
getOutsideJustification(outsideJustification, isRTL);
switch (horizontalJustification) {
case _HorizontalJustification.leftDrawArea:
behaviorOffset =
new Offset(behavior.drawAreaBounds.left.toDouble(), heightOffset);
break;
case _HorizontalJustification.left:
behaviorOffset = new Offset(0.0, heightOffset);
break;
case _HorizontalJustification.rightDrawArea:
behaviorOffset = new Offset(
behavior.drawAreaBounds.right - behaviorSize.width, heightOffset);
break;
case _HorizontalJustification.right:
behaviorOffset =
new Offset(chartSize.width - behaviorSize.width, heightOffset);
break;
}
} else if (behaviorPosition == common.BehaviorPosition.start ||
behaviorPosition == common.BehaviorPosition.end) {
final widthOffset =
(isRTL && behaviorPosition == common.BehaviorPosition.start) ||
(!isRTL && behaviorPosition == common.BehaviorPosition.end)
? chartSize.width
: 0.0;
switch (outsideJustification) {
case common.OutsideJustification.startDrawArea:
case common.OutsideJustification.middleDrawArea:
behaviorOffset =
new Offset(widthOffset, behavior.drawAreaBounds.top.toDouble());
break;
case common.OutsideJustification.start:
case common.OutsideJustification.middle:
behaviorOffset = new Offset(widthOffset, 0.0);
break;
case common.OutsideJustification.endDrawArea:
behaviorOffset = new Offset(widthOffset,
behavior.drawAreaBounds.bottom - behaviorSize.height);
break;
case common.OutsideJustification.end:
behaviorOffset =
new Offset(widthOffset, chartSize.height - behaviorSize.height);
break;
}
} else if (behaviorPosition == common.BehaviorPosition.inside) {
var rightOffset = new Offset(chartSize.width - behaviorSize.width, 0.0);
switch (insideJustification) {
case common.InsideJustification.topStart:
behaviorOffset = isRTL ? rightOffset : Offset.zero;
break;
case common.InsideJustification.topEnd:
behaviorOffset = isRTL ? Offset.zero : rightOffset;
break;
}
}
return behaviorOffset;
}
_HorizontalJustification getOutsideJustification(
common.OutsideJustification justification, bool isRTL) {
_HorizontalJustification mappedJustification;
switch (justification) {
case common.OutsideJustification.startDrawArea:
case common.OutsideJustification.middleDrawArea:
mappedJustification = isRTL
? _HorizontalJustification.rightDrawArea
: _HorizontalJustification.leftDrawArea;
break;
case common.OutsideJustification.start:
case common.OutsideJustification.middle:
mappedJustification = isRTL
? _HorizontalJustification.right
: _HorizontalJustification.left;
break;
case common.OutsideJustification.endDrawArea:
mappedJustification = isRTL
? _HorizontalJustification.leftDrawArea
: _HorizontalJustification.rightDrawArea;
break;
case common.OutsideJustification.end:
mappedJustification = isRTL
? _HorizontalJustification.left
: _HorizontalJustification.right;
break;
}
return mappedJustification;
}
}
enum _HorizontalJustification {
leftDrawArea,
left,
rightDrawArea,
right,
}

View File

@@ -0,0 +1,417 @@
# Generated by pub
# See https://www.dartlang.org/tools/pub/glossary#lockfile
packages:
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.36.3"
args:
dependency: transitive
description:
name: args
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.1"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
charts_common:
dependency: "direct main"
description:
path: "../common"
relative: true
source: path
version: "0.6.0"
collection:
dependency: "direct main"
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.11"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.0"
flutter_web:
dependency: "direct main"
description:
path: "packages/flutter_web"
ref: HEAD
resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4"
url: "https://github.com/flutter/flutter_web"
source: git
version: "0.0.0"
flutter_web_test:
dependency: "direct dev"
description:
path: "packages/flutter_web_test"
ref: HEAD
resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4"
url: "https://github.com/flutter/flutter_web"
source: git
version: "0.0.0"
flutter_web_ui:
dependency: "direct overridden"
description:
path: "packages/flutter_web_ui"
ref: HEAD
resolved-ref: "7a92f7391ee8a72c398f879e357380084e2076b4"
url: "https://github.com/flutter/flutter_web"
source: git
version: "0.0.0"
front_end:
dependency: transitive
description:
name: front_end
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.18"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.7"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.0+2"
http:
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.0+2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.3"
intl:
dependency: "direct main"
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.8"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+1"
json_rpc_2:
dependency: transitive
description:
name: json_rpc_2
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
kernel:
dependency: transitive
description:
name: kernel
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.18"
logging:
dependency: "direct main"
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.3+2"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.5"
meta:
dependency: "direct main"
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.7"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.6+2"
mockito:
dependency: "direct dev"
description:
name: mockito
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
multi_server_socket:
dependency: transitive
description:
name: multi_server_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
package_resolver:
dependency: transitive
description:
name: package_resolver
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.10"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.2"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.0"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.2"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.5"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.8"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.3"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.5"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.8"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.5"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.3"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.5"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.5"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
vm_service_client:
dependency: transitive
description:
name: vm_service_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.6+2"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+10"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.12"
yaml:
dependency: transitive
description:
name: yaml
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.15"
sdks:
dart: ">=2.2.0 <3.0.0"

View File

@@ -0,0 +1,45 @@
name: charts_flutter
version: 0.6.0
description: Material Design charting library for flutter.
author: Charts Team <charts_flutter@google.com>
homepage: https://github.com/google/charts
environment:
sdk: '>=2.0.0 <3.0.0'
dependencies:
# Pointing this to a local path allows for pointing to the latest code
# in Github for open source development.
#
# The pub version of charts_flutter will point to the pub version of charts_common.
# The latest pub version is commented out and shown below as an example.
# charts_common: 0.6.0
charts_common:
path: ../common/
collection: ^1.14.5
flutter_web: any
intl: ^0.15.2
logging: any
meta: ^1.1.1
dev_dependencies:
mockito:
flutter_web_test: any
test: ^1.3.0
# flutter_web packages are not published to pub.dartlang.org
# These overrides tell the package tools to get them from GitHub
dependency_overrides:
flutter_web:
git:
url: https://github.com/flutter/flutter_web
path: packages/flutter_web
flutter_web_test:
git:
url: https://github.com/flutter/flutter_web
path: packages/flutter_web_test
flutter_web_ui:
git:
url: https://github.com/flutter/flutter_web
path: packages/flutter_web_ui

View File

@@ -0,0 +1,116 @@
// 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/material.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:charts_flutter/src/behaviors/legend/legend_layout.dart';
class MockContext extends Mock implements BuildContext {}
void main() {
BuildContext context;
setUp(() {
context = new MockContext();
});
group('TabularLegendLayoutBuilder', () {
test('builds horizontally', () {
final builder = new TabularLegendLayout.horizontalFirst();
final widgets = <Widget>[new Text('1'), new Text('2'), new Text('3')];
final Table layout = builder.build(context, widgets);
expect(layout.children.length, 1);
expect(layout.children.first.children.length, 3);
});
test('does not build extra columns if max columns exceed widget count', () {
final builder =
new TabularLegendLayout.horizontalFirst(desiredMaxColumns: 10);
final widgets = <Widget>[new Text('1'), new Text('2'), new Text('3')];
final Table layout = builder.build(context, widgets);
expect(layout.children.length, 1);
expect(layout.children.first.children.length, 3);
});
test('builds horizontally until max column exceeded', () {
final builder =
new TabularLegendLayout.horizontalFirst(desiredMaxColumns: 2);
final widgets = new List<Widget>.generate(
7, (int index) => new Text(index.toString()));
final Table layout = builder.build(context, widgets);
expect(layout.children.length, 4);
expect(layout.children[0].children[0], equals(widgets[0]));
expect(layout.children[0].children[1], equals(widgets[1]));
expect(layout.children[1].children[0], equals(widgets[2]));
expect(layout.children[1].children[1], equals(widgets[3]));
expect(layout.children[2].children[0], equals(widgets[4]));
expect(layout.children[2].children[1], equals(widgets[5]));
expect(layout.children[3].children[0], equals(widgets[6]));
});
test('builds vertically', () {
final builder = new TabularLegendLayout.verticalFirst();
final widgets = <Widget>[new Text('1'), new Text('2'), new Text('3')];
final Table layout = builder.build(context, widgets);
expect(layout.children.length, 3);
expect(layout.children[0].children.length, 1);
expect(layout.children[1].children.length, 1);
expect(layout.children[2].children.length, 1);
});
test('does not build extra rows if max rows exceed widget count', () {
final builder = new TabularLegendLayout.verticalFirst(desiredMaxRows: 10);
final widgets = <Widget>[new Text('1'), new Text('2'), new Text('3')];
final Table layout = builder.build(context, widgets);
expect(layout.children.length, 3);
expect(layout.children[0].children.length, 1);
expect(layout.children[1].children.length, 1);
expect(layout.children[2].children.length, 1);
});
test('builds vertically until max column exceeded', () {
final builder = new TabularLegendLayout.verticalFirst(desiredMaxRows: 2);
final widgets = new List<Widget>.generate(
7, (int index) => new Text(index.toString()));
final Table layout = builder.build(context, widgets);
expect(layout.children.length, 2);
expect(layout.children[0].children[0], equals(widgets[0]));
expect(layout.children[1].children[0], equals(widgets[1]));
expect(layout.children[0].children[1], equals(widgets[2]));
expect(layout.children[1].children[1], equals(widgets[3]));
expect(layout.children[0].children[2], equals(widgets[4]));
expect(layout.children[1].children[2], equals(widgets[5]));
expect(layout.children[0].children[3], equals(widgets[6]));
});
});
}

View File

@@ -0,0 +1,39 @@
// 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/material.dart' show BuildContext;
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:charts_flutter/src/graphics_factory.dart';
import 'package:charts_flutter/src/text_element.dart';
class MockContext extends Mock implements BuildContext {}
class MockGraphicsFactoryHelper extends Mock implements GraphicsFactoryHelper {}
void main() {
test('Text element gets assigned scale factor', () {
final helper = new MockGraphicsFactoryHelper();
when(helper.getTextScaleFactorOf(any)).thenReturn(3.0);
final graphicsFactory =
new GraphicsFactory(new MockContext(), helper: helper);
final textElement =
graphicsFactory.createTextElement('test') as TextElement;
expect(textElement.text, equals('test'));
expect(textElement.textScaleFactor, equals(3.0));
});
}

View File

@@ -0,0 +1,128 @@
// 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';
import 'package:flutter_web_test/flutter_web_test.dart';
import 'package:charts_flutter/flutter.dart' as charts;
void main() {
testWidgets('selection can be set programmatically',
(WidgetTester tester) async {
final onTapSelection =
new charts.UserManagedSelectionModel<String>.fromConfig(
selectedDataConfig: [
new charts.SeriesDatumConfig<String>('Sales', '2016')
]);
charts.SelectionModel<String> currentSelectionModel;
void selectionChangedListener(charts.SelectionModel<String> model) {
currentSelectionModel = model;
}
final testChart = new TestChart(selectionChangedListener, onTapSelection);
await tester.pumpWidget(testChart);
expect(currentSelectionModel, isNull);
await tester.tap(find.byType(charts.BarChart));
await tester.pump();
expect(currentSelectionModel.selectedDatum, hasLength(1));
final selectedDatum =
currentSelectionModel.selectedDatum.first.datum as OrdinalSales;
expect(selectedDatum.year, equals('2016'));
expect(selectedDatum.sales, equals(100));
expect(currentSelectionModel.selectedSeries, hasLength(1));
expect(currentSelectionModel.selectedSeries.first.id, equals('Sales'));
});
}
class TestChart extends StatefulWidget {
final charts.SelectionModelListener<String> selectionChangedListener;
final charts.UserManagedSelectionModel<String> onTapSelection;
TestChart(this.selectionChangedListener, this.onTapSelection);
@override
TestChartState createState() {
return new TestChartState(selectionChangedListener, onTapSelection);
}
}
class TestChartState extends State<TestChart> {
final charts.SelectionModelListener<String> selectionChangedListener;
final charts.UserManagedSelectionModel<String> onTapSelection;
final seriesList = _createSampleData();
final myState = new charts.UserManagedState<String>();
TestChartState(this.selectionChangedListener, this.onTapSelection);
@override
Widget build(BuildContext context) {
final chart = new charts.BarChart(
seriesList,
userManagedState: myState,
selectionModels: [
new charts.SelectionModelConfig(
type: charts.SelectionModelType.info,
changedListener: widget.selectionChangedListener)
],
// Disable animation and gesture for testing.
animate: false, //widget.animate,
defaultInteractions: false,
);
return new GestureDetector(child: chart, onTap: handleOnTap);
}
void handleOnTap() {
setState(() {
myState.selectionModels[charts.SelectionModelType.info] = onTapSelection;
});
}
}
/// Create one series with sample hard coded data.
List<charts.Series<OrdinalSales, String>> _createSampleData() {
final data = [
new OrdinalSales('2014', 5),
new OrdinalSales('2015', 25),
new OrdinalSales('2016', 100),
new OrdinalSales('2017', 75),
];
return [
new charts.Series<OrdinalSales, String>(
id: 'Sales',
colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
domainFn: (OrdinalSales sales, _) => sales.year,
measureFn: (OrdinalSales sales, _) => sales.sales,
data: data,
)
];
}
/// Sample ordinal data type.
class OrdinalSales {
final String year;
final int sales;
OrdinalSales(this.year, this.sales);
}

View File

@@ -0,0 +1,548 @@
// 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:flutter_web/material.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter_web_test/flutter_web_test.dart';
import 'package:charts_common/common.dart' as common
show BehaviorPosition, InsideJustification, OutsideJustification;
import 'package:charts_flutter/src/behaviors/chart_behavior.dart';
import 'package:charts_flutter/src/widget_layout_delegate.dart';
const chartContainerLayoutID = 'chartContainer';
class MockBuildableBehavior extends Mock implements BuildableBehavior {}
void main() {
group('widget layout test', () {
final chartKey = new UniqueKey();
final behaviorKey = new UniqueKey();
final behaviorID = 'behavior';
final totalSize = const Size(200.0, 100.0);
final behaviorSize = const Size(50.0, 50.0);
/// Creates widget for testing.
Widget createWidget(
Size chartSize, Size behaviorSize, common.BehaviorPosition position,
{common.OutsideJustification outsideJustification,
common.InsideJustification insideJustification,
Rectangle<int> drawAreaBounds,
bool isRTL: false}) {
// Create a mock buildable behavior that returns information about the
// position and justification desired.
final behavior = new MockBuildableBehavior();
when(behavior.position).thenReturn(position);
when(behavior.outsideJustification).thenReturn(outsideJustification);
when(behavior.insideJustification).thenReturn(insideJustification);
when(behavior.drawAreaBounds).thenReturn(drawAreaBounds);
// The 'chart' widget that expands to the full size allowed to test that
// the behavior widget's size affects the size given to the chart.
final chart = new LayoutId(
key: chartKey, id: chartContainerLayoutID, child: new Container());
// A behavior widget
final behaviorWidget = new LayoutId(
key: behaviorKey,
id: behaviorID,
child: new SizedBox.fromSize(size: behaviorSize));
// Create a the widget that uses the layout delegate that is being tested.
final layout = new CustomMultiChildLayout(
delegate: new WidgetLayoutDelegate(
chartContainerLayoutID, {behaviorID: behavior}, isRTL),
children: [chart, behaviorWidget]);
final container = new Align(
alignment: Alignment.topLeft,
child: new Container(
width: chartSize.width, height: chartSize.height, child: layout));
return container;
}
// Verifies the expected results.
void verifyResults(WidgetTester tester, Size expectedChartSize,
Offset expectedChartOffset, Offset expectedBehaviorOffset) {
final RenderBox chartBox = tester.firstRenderObject(find.byKey(chartKey));
expect(chartBox.size, equals(expectedChartSize));
final chartOffset = chartBox.localToGlobal(Offset.zero);
expect(chartOffset, equals(expectedChartOffset));
final RenderBox behaviorBox =
tester.firstRenderObject(find.byKey(behaviorKey));
final behaviorOffset = behaviorBox.localToGlobal(Offset.zero);
expect(behaviorOffset, equals(expectedBehaviorOffset));
}
testWidgets('Position top - start draw area justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.top;
final outsideJustification = common.OutsideJustification.startDrawArea;
final drawAreaBounds = const Rectangle<int>(25, 50, 150, 50);
// Behavior takes up 50 height, so 50 height remains for the chart.
final expectedChartSize = const Size(200.0, 50.0);
// Behavior is positioned on the top, so the chart is offset by 50.
final expectedChartOffset = const Offset(0.0, 50.0);
// Behavior is aligned to draw area
final expectedBehaviorOffset = const Offset(25.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('Position bottom - end draw area justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.bottom;
final outsideJustification = common.OutsideJustification.endDrawArea;
final drawAreaBounds = const Rectangle<int>(25, 0, 125, 50);
// Behavior takes up 50 height, so 50 height remains for the chart.
final expectedChartSize = const Size(200.0, 50.0);
// Behavior is positioned on the bottom, so the chart is offset by 0.
final expectedChartOffset = const Offset(0.0, 0.0);
// Behavior is aligned to draw area and offset to the bottom.
final expectedBehaviorOffset = const Offset(100.0, 50.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('Position start - start draw area justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.start;
final outsideJustification = common.OutsideJustification.startDrawArea;
final drawAreaBounds = const Rectangle<int>(75, 25, 150, 50);
// Behavior takes up 50 width, so 150 width remains for the chart.
final expectedChartSize = const Size(150.0, 100.0);
// Behavior is positioned at the start (left) since this is NOT a RTL
// so the chart is offset to the right by the behavior width of 50.
final expectedChartOffset = const Offset(50.0, 0.0);
// Behavior is aligned to draw area.
final expectedBehaviorOffset = const Offset(0.0, 25.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('Position end - end draw area justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.end;
final outsideJustification = common.OutsideJustification.endDrawArea;
final drawAreaBounds = const Rectangle<int>(25, 25, 150, 50);
// Behavior takes up 50 width, so 150 width remains for the chart.
final expectedChartSize = const Size(150.0, 100.0);
// Behavior is positioned at the right (left) since this is NOT a RTL
// so no offset for the chart.
final expectedChartOffset = const Offset(0.0, 0.0);
// Behavior is aligned to draw area and offset to the right of the
// chart.
final expectedBehaviorOffset = const Offset(150.0, 25.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('Position top - start justified', (WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.top;
final outsideJustification = common.OutsideJustification.start;
final drawAreaBounds = const Rectangle<int>(25, 50, 150, 50);
// Behavior takes up 50 height, so 50 height remains for the chart.
final expectedChartSize = const Size(200.0, 50.0);
// Behavior is positioned on the top, so the chart is offset by 50.
final expectedChartOffset = const Offset(0.0, 50.0);
// Behavior is aligned to the start, so no offset
final expectedBehaviorOffset = const Offset(0.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('Position top - end justified', (WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.top;
final outsideJustification = common.OutsideJustification.end;
final drawAreaBounds = const Rectangle<int>(25, 50, 150, 50);
// Behavior takes up 50 height, so 50 height remains for the chart.
final expectedChartSize = const Size(200.0, 50.0);
// Behavior is positioned on the top, so the chart is offset by 50.
final expectedChartOffset = const Offset(0.0, 50.0);
// Behavior is aligned to the end, so it is offset by total size minus
// the behavior size.
final expectedBehaviorOffset = const Offset(150.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('Position start - start justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.start;
final outsideJustification = common.OutsideJustification.start;
final drawAreaBounds = const Rectangle<int>(75, 25, 150, 50);
// Behavior takes up 50 width, so 150 width remains for the chart.
final expectedChartSize = const Size(150.0, 100.0);
// Behavior is positioned at the start (left) since this is NOT a RTL
// so the chart is offset to the right by the behavior width of 50.
final expectedChartOffset = const Offset(50.0, 0.0);
// No offset because it is start justified.
final expectedBehaviorOffset = const Offset(0.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('Position start - end justified', (WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.start;
final outsideJustification = common.OutsideJustification.end;
final drawAreaBounds = const Rectangle<int>(75, 25, 150, 50);
// Behavior takes up 50 width, so 150 width remains for the chart.
final expectedChartSize = const Size(150.0, 100.0);
// Behavior is positioned at the start (left) since this is NOT a RTL
// so the chart is offset to the right by the behavior width of 50.
final expectedChartOffset = const Offset(50.0, 0.0);
// End justified, total height minus behavior height
final expectedBehaviorOffset = const Offset(0.0, 50.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('Position inside - top start justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.inside;
final insideJustification = common.InsideJustification.topStart;
final drawAreaBounds = const Rectangle<int>(25, 25, 175, 75);
// Behavior is layered on top, chart uses the full size.
final expectedChartSize = const Size(200.0, 100.0);
// No offset since chart takes up full size.
final expectedChartOffset = const Offset(0.0, 0.0);
// Top start justified, no offset
final expectedBehaviorOffset = const Offset(0.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
insideJustification: insideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('Position inside - top end justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.inside;
final insideJustification = common.InsideJustification.topEnd;
final drawAreaBounds = const Rectangle<int>(25, 25, 175, 75);
// Behavior is layered on top, chart uses the full size.
final expectedChartSize = const Size(200.0, 100.0);
// No offset since chart takes up full size.
final expectedChartOffset = const Offset(0.0, 0.0);
// Offset to the top end
final expectedBehaviorOffset = const Offset(150.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
insideJustification: insideJustification,
drawAreaBounds: drawAreaBounds));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position top - start draw area justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.top;
final outsideJustification = common.OutsideJustification.startDrawArea;
final drawAreaBounds = const Rectangle<int>(0, 50, 175, 50);
// Behavior takes up 50 height, so 50 height remains for the chart.
final expectedChartSize = const Size(200.0, 50.0);
// Behavior is positioned on the top, so the chart is offset by 50.
final expectedChartOffset = const Offset(0.0, 50.0);
// Behavior is aligned to start draw area, which is to the left in RTL
final expectedBehaviorOffset = const Offset(125.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position bottom - end draw area justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.bottom;
final outsideJustification = common.OutsideJustification.endDrawArea;
final drawAreaBounds = const Rectangle<int>(0, 0, 175, 50);
// Behavior takes up 50 height, so 50 height remains for the chart.
final expectedChartSize = const Size(200.0, 50.0);
// Behavior is positioned on the bottom, so the chart is offset by 0.
final expectedChartOffset = const Offset(0.0, 0.0);
// Behavior is aligned to end draw area (left) and offset to the bottom.
final expectedBehaviorOffset = const Offset(0.0, 50.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position start - start draw area justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.start;
final outsideJustification = common.OutsideJustification.startDrawArea;
final drawAreaBounds = const Rectangle<int>(0, 25, 125, 75);
// Behavior takes up 50 width, so 150 width remains for the chart.
final expectedChartSize = const Size(150.0, 100.0);
// Chart is on the left, so no offset.
final expectedChartOffset = const Offset(0.0, 0.0);
// Behavior is positioned at the start (right) and start draw area.
final expectedBehaviorOffset = const Offset(150.0, 25.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position end - end draw area justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.end;
final outsideJustification = common.OutsideJustification.endDrawArea;
final drawAreaBounds = const Rectangle<int>(75, 25, 125, 75);
// Behavior takes up 50 width, so 150 width remains for the chart.
final expectedChartSize = const Size(150.0, 100.0);
// Chart is to the left of the behavior because of RTL.
final expectedChartOffset = const Offset(50.0, 0.0);
// Behavior is aligned to end draw area.
final expectedBehaviorOffset = const Offset(0.0, 50.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position top - start justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.top;
final outsideJustification = common.OutsideJustification.start;
final drawAreaBounds = const Rectangle<int>(25, 50, 150, 50);
// Behavior takes up 50 height, so 50 height remains for the chart.
final expectedChartSize = const Size(200.0, 50.0);
// Behavior is positioned on the top, so the chart is offset by 50.
final expectedChartOffset = const Offset(0.0, 50.0);
// Behavior is aligned to the end, offset by behavior size.
final expectedBehaviorOffset = const Offset(150.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position top - end justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.top;
final outsideJustification = common.OutsideJustification.end;
final drawAreaBounds = const Rectangle<int>(25, 50, 150, 50);
// Behavior takes up 50 height, so 50 height remains for the chart.
final expectedChartSize = const Size(200.0, 50.0);
// Behavior is positioned on the top, so the chart is offset by 50.
final expectedChartOffset = const Offset(0.0, 50.0);
// Behavior is aligned to the end, no offset.
final expectedBehaviorOffset = const Offset(0.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position start - start justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.start;
final outsideJustification = common.OutsideJustification.start;
final drawAreaBounds = const Rectangle<int>(75, 25, 150, 50);
// Behavior takes up 50 width, so 150 width remains for the chart.
final expectedChartSize = const Size(150.0, 100.0);
// Behavior is positioned at the right since this is RTL so the chart is
// has no offset.
final expectedChartOffset = const Offset(0.0, 0.0);
// No offset because it is start justified.
final expectedBehaviorOffset = const Offset(150.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position start - end justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.start;
final outsideJustification = common.OutsideJustification.end;
final drawAreaBounds = const Rectangle<int>(75, 25, 150, 50);
// Behavior takes up 50 width, so 150 width remains for the chart.
final expectedChartSize = const Size(150.0, 100.0);
// Behavior is positioned at the right since this is RTL so the chart is
// has no offset.
final expectedChartOffset = const Offset(0.0, 0.0);
// End justified, total height minus behavior height
final expectedBehaviorOffset = const Offset(150.0, 50.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
outsideJustification: outsideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position inside - top start justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.inside;
final insideJustification = common.InsideJustification.topStart;
final drawAreaBounds = const Rectangle<int>(25, 25, 175, 75);
// Behavior is layered on top, chart uses the full size.
final expectedChartSize = const Size(200.0, 100.0);
// No offset since chart takes up full size.
final expectedChartOffset = const Offset(0.0, 0.0);
// Offset to the right
final expectedBehaviorOffset = const Offset(150.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
insideJustification: insideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
testWidgets('RTL - Position inside - top end justified',
(WidgetTester tester) async {
final behaviorPosition = common.BehaviorPosition.inside;
final insideJustification = common.InsideJustification.topEnd;
final drawAreaBounds = const Rectangle<int>(25, 25, 175, 75);
// Behavior is layered on top, chart uses the full size.
final expectedChartSize = const Size(200.0, 100.0);
// No offset since chart takes up full size.
final expectedChartOffset = const Offset(0.0, 0.0);
// No offset, since end is to the left.
final expectedBehaviorOffset = const Offset(0.0, 0.0);
await tester.pumpWidget(createWidget(
totalSize, behaviorSize, behaviorPosition,
insideJustification: insideJustification,
drawAreaBounds: drawAreaBounds,
isRTL: true));
verifyResults(tester, expectedChartSize, expectedChartOffset,
expectedBehaviorOffset);
});
});
}