mirror of
https://github.com/flutter/samples.git
synced 2025-11-09 14:28:51 +00:00
Update web/ samples to work with Flutter SDK (#134)
* add package:http dependency in dad_jokes * add package:http dependency in filipino_cuisine * don't build package:http demos until flutter/flutter#34858 is resolved * update gallery * update github_dataviz * update particle_background * don't build github_dataviz (uses package:http) * update slide_puzzle * update spinning_square * update timeflow * update vision_challenge * update charts * update dad_jokes * update filipino cuisine * ignore build output * update timeflow and vision_challenge * update slide_puzzle * don't commit build/ directory * move preview.png images to assets * fix path url join * update readme * update web/readme.md
This commit is contained in:
1
web/.gitignore
vendored
1
web/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
!**/pubspec.lock
|
!**/pubspec.lock
|
||||||
|
*/build
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class _Demo {
|
|||||||
String get html => '''
|
String get html => '''
|
||||||
<div>
|
<div>
|
||||||
<a href='$buildDir'>
|
<a href='$buildDir'>
|
||||||
<img src='${p.url.join(buildDir, 'preview.png')}' width="300" alt="$name">
|
<img src='${p.url.join(buildDir, 'assets/assets/preview.png')}' width="300" alt="$name">
|
||||||
</a>
|
</a>
|
||||||
<a class='demo-title' href='$buildDir'>$name</a>
|
<a class='demo-title' href='$buildDir'>$name</a>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
1
web/charts/common/.gitignore
vendored
1
web/charts/common/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
build/
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# 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
|
|
||||||
* 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.
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
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.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Common Charting library
|
|
||||||
|
|
||||||
[](https://pub.dartlang.org/packages/charts_common)
|
|
||||||
|
|
||||||
Common componnets for charting libraries.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
include: package:pedantic/analysis_options.yaml
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
# strong-mode:
|
|
||||||
# implicit-casts: false
|
|
||||||
# implicit-dynamic: false
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
- avoid_types_on_closure_parameters
|
|
||||||
- avoid_void_async
|
|
||||||
- await_only_futures
|
|
||||||
- camel_case_types
|
|
||||||
- cancel_subscriptions
|
|
||||||
- close_sinks
|
|
||||||
# TODO(domesticmouse): rename constants
|
|
||||||
# - constant_identifier_names
|
|
||||||
- control_flow_in_finally
|
|
||||||
- empty_statements
|
|
||||||
# TODO(domesticmouse): implement hashCode methods
|
|
||||||
# - hash_and_equals
|
|
||||||
- implementation_imports
|
|
||||||
- non_constant_identifier_names
|
|
||||||
- package_api_docs
|
|
||||||
- package_names
|
|
||||||
- package_prefixed_library_names
|
|
||||||
- test_types_in_equals
|
|
||||||
- throw_in_finally
|
|
||||||
- unnecessary_brace_in_string_interps
|
|
||||||
- unnecessary_getters_setters
|
|
||||||
- unnecessary_new
|
|
||||||
- unnecessary_statements
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
export 'src/chart/bar/bar_chart.dart' show BarChart;
|
|
||||||
export 'src/chart/bar/bar_label_decorator.dart'
|
|
||||||
show BarLabelAnchor, BarLabelDecorator, BarLabelPosition;
|
|
||||||
export 'src/chart/bar/bar_lane_renderer_config.dart' show BarLaneRendererConfig;
|
|
||||||
export 'src/chart/bar/bar_renderer.dart'
|
|
||||||
show BarRenderer, ImmutableBarRendererElement;
|
|
||||||
export 'src/chart/bar/bar_renderer_config.dart'
|
|
||||||
show
|
|
||||||
BarRendererConfig,
|
|
||||||
CornerStrategy,
|
|
||||||
ConstCornerStrategy,
|
|
||||||
NoCornerStrategy;
|
|
||||||
export 'src/chart/bar/bar_renderer_decorator.dart' show BarRendererDecorator;
|
|
||||||
export 'src/chart/bar/bar_target_line_renderer.dart' show BarTargetLineRenderer;
|
|
||||||
export 'src/chart/bar/bar_target_line_renderer_config.dart'
|
|
||||||
show BarTargetLineRendererConfig;
|
|
||||||
export 'src/chart/bar/base_bar_renderer_config.dart'
|
|
||||||
show BarGroupingType, BaseBarRendererConfig;
|
|
||||||
export 'src/chart/cartesian/axis/axis.dart'
|
|
||||||
show
|
|
||||||
domainAxisKey,
|
|
||||||
measureAxisIdKey,
|
|
||||||
measureAxisKey,
|
|
||||||
Axis,
|
|
||||||
NumericAxis,
|
|
||||||
OrdinalAxis,
|
|
||||||
OrdinalViewport;
|
|
||||||
export 'src/chart/cartesian/axis/numeric_extents.dart' show NumericExtents;
|
|
||||||
export 'src/chart/cartesian/axis/draw_strategy/gridline_draw_strategy.dart'
|
|
||||||
show GridlineRendererSpec;
|
|
||||||
export 'src/chart/cartesian/axis/draw_strategy/none_draw_strategy.dart'
|
|
||||||
show NoneRenderSpec;
|
|
||||||
export 'src/chart/cartesian/axis/draw_strategy/small_tick_draw_strategy.dart'
|
|
||||||
show SmallTickRendererSpec;
|
|
||||||
export 'src/chart/cartesian/axis/tick_formatter.dart'
|
|
||||||
show SimpleTickFormatterBase, TickFormatter;
|
|
||||||
export 'src/chart/cartesian/axis/spec/axis_spec.dart'
|
|
||||||
show
|
|
||||||
AxisSpec,
|
|
||||||
LineStyleSpec,
|
|
||||||
RenderSpec,
|
|
||||||
TextStyleSpec,
|
|
||||||
TickLabelAnchor,
|
|
||||||
TickLabelJustification,
|
|
||||||
TickFormatterSpec;
|
|
||||||
export 'src/chart/cartesian/axis/spec/bucketing_axis_spec.dart'
|
|
||||||
show BucketingAxisSpec, BucketingNumericTickProviderSpec;
|
|
||||||
export 'src/chart/cartesian/axis/spec/date_time_axis_spec.dart'
|
|
||||||
show
|
|
||||||
DateTimeAxisSpec,
|
|
||||||
DayTickProviderSpec,
|
|
||||||
AutoDateTimeTickFormatterSpec,
|
|
||||||
AutoDateTimeTickProviderSpec,
|
|
||||||
DateTimeEndPointsTickProviderSpec,
|
|
||||||
DateTimeTickFormatterSpec,
|
|
||||||
DateTimeTickProviderSpec,
|
|
||||||
TimeFormatterSpec,
|
|
||||||
StaticDateTimeTickProviderSpec;
|
|
||||||
export 'src/chart/cartesian/axis/spec/end_points_time_axis_spec.dart'
|
|
||||||
show EndPointsTimeAxisSpec;
|
|
||||||
export 'src/chart/cartesian/axis/spec/numeric_axis_spec.dart'
|
|
||||||
show
|
|
||||||
NumericAxisSpec,
|
|
||||||
NumericEndPointsTickProviderSpec,
|
|
||||||
NumericTickProviderSpec,
|
|
||||||
NumericTickFormatterSpec,
|
|
||||||
BasicNumericTickFormatterSpec,
|
|
||||||
BasicNumericTickProviderSpec,
|
|
||||||
StaticNumericTickProviderSpec;
|
|
||||||
export 'src/chart/cartesian/axis/spec/ordinal_axis_spec.dart'
|
|
||||||
show
|
|
||||||
BasicOrdinalTickProviderSpec,
|
|
||||||
BasicOrdinalTickFormatterSpec,
|
|
||||||
OrdinalAxisSpec,
|
|
||||||
OrdinalTickFormatterSpec,
|
|
||||||
OrdinalTickProviderSpec,
|
|
||||||
StaticOrdinalTickProviderSpec;
|
|
||||||
export 'src/chart/cartesian/axis/spec/percent_axis_spec.dart'
|
|
||||||
show PercentAxisSpec;
|
|
||||||
export 'src/chart/cartesian/axis/time/date_time_extents.dart'
|
|
||||||
show DateTimeExtents;
|
|
||||||
export 'src/chart/cartesian/axis/time/date_time_tick_formatter.dart'
|
|
||||||
show DateTimeTickFormatter;
|
|
||||||
export 'src/chart/cartesian/axis/spec/tick_spec.dart' show TickSpec;
|
|
||||||
export 'src/chart/cartesian/cartesian_chart.dart'
|
|
||||||
show CartesianChart, NumericCartesianChart, OrdinalCartesianChart;
|
|
||||||
export 'src/chart/cartesian/cartesian_renderer.dart' show BaseCartesianRenderer;
|
|
||||||
export 'src/chart/common/base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
export 'src/chart/common/behavior/a11y/a11y_explore_behavior.dart'
|
|
||||||
show ExploreModeTrigger;
|
|
||||||
export 'src/chart/common/behavior/a11y/a11y_node.dart' show A11yNode;
|
|
||||||
export 'src/chart/common/behavior/a11y/domain_a11y_explore_behavior.dart'
|
|
||||||
show DomainA11yExploreBehavior, VocalizationCallback;
|
|
||||||
export 'src/chart/common/behavior/chart_behavior.dart'
|
|
||||||
show
|
|
||||||
BehaviorPosition,
|
|
||||||
ChartBehavior,
|
|
||||||
InsideJustification,
|
|
||||||
OutsideJustification;
|
|
||||||
export 'src/chart/common/behavior/calculation/percent_injector.dart'
|
|
||||||
show PercentInjector, PercentInjectorTotalType;
|
|
||||||
export 'src/chart/common/behavior/domain_highlighter.dart'
|
|
||||||
show DomainHighlighter;
|
|
||||||
export 'src/chart/common/behavior/initial_selection.dart' show InitialSelection;
|
|
||||||
export 'src/chart/common/behavior/legend/legend.dart'
|
|
||||||
show Legend, LegendCellPadding, LegendState, LegendTapHandling;
|
|
||||||
export 'src/chart/common/behavior/legend/legend_entry.dart' show LegendEntry;
|
|
||||||
export 'src/chart/common/behavior/legend/legend_entry_generator.dart'
|
|
||||||
show LegendEntryGenerator, LegendDefaultMeasure;
|
|
||||||
export 'src/chart/common/behavior/legend/datum_legend.dart' show DatumLegend;
|
|
||||||
export 'src/chart/common/behavior/legend/series_legend.dart' show SeriesLegend;
|
|
||||||
export 'src/chart/common/behavior/line_point_highlighter.dart'
|
|
||||||
show LinePointHighlighter, LinePointHighlighterFollowLineType;
|
|
||||||
export 'src/chart/common/behavior/range_annotation.dart'
|
|
||||||
show
|
|
||||||
AnnotationLabelAnchor,
|
|
||||||
AnnotationLabelDirection,
|
|
||||||
AnnotationLabelPosition,
|
|
||||||
AnnotationSegment,
|
|
||||||
LineAnnotationSegment,
|
|
||||||
RangeAnnotation,
|
|
||||||
RangeAnnotationAxisType,
|
|
||||||
RangeAnnotationSegment;
|
|
||||||
export 'src/chart/common/behavior/sliding_viewport.dart' show SlidingViewport;
|
|
||||||
export 'src/chart/common/behavior/chart_title/chart_title.dart'
|
|
||||||
show ChartTitle, ChartTitleDirection;
|
|
||||||
export 'src/chart/common/behavior/selection/lock_selection.dart'
|
|
||||||
show LockSelection;
|
|
||||||
export 'src/chart/common/behavior/selection/select_nearest.dart'
|
|
||||||
show SelectNearest;
|
|
||||||
export 'src/chart/common/behavior/selection/selection_trigger.dart'
|
|
||||||
show SelectionTrigger;
|
|
||||||
export 'src/chart/common/behavior/slider/slider.dart'
|
|
||||||
show
|
|
||||||
Slider,
|
|
||||||
SliderHandlePosition,
|
|
||||||
SliderListenerCallback,
|
|
||||||
SliderListenerDragState,
|
|
||||||
SliderStyle;
|
|
||||||
export 'src/chart/common/behavior/zoom/initial_hint_behavior.dart'
|
|
||||||
show InitialHintBehavior;
|
|
||||||
export 'src/chart/common/behavior/zoom/pan_and_zoom_behavior.dart'
|
|
||||||
show PanAndZoomBehavior;
|
|
||||||
export 'src/chart/common/behavior/zoom/pan_behavior.dart'
|
|
||||||
show PanBehavior, PanningCompletedCallback;
|
|
||||||
export 'src/chart/common/behavior/zoom/panning_tick_provider.dart'
|
|
||||||
show PanningTickProviderMode;
|
|
||||||
export 'src/chart/common/canvas_shapes.dart'
|
|
||||||
show CanvasBarStack, CanvasPie, CanvasPieSlice, CanvasRect;
|
|
||||||
export 'src/chart/common/chart_canvas.dart' show ChartCanvas, FillPatternType;
|
|
||||||
export 'src/chart/common/chart_context.dart' show ChartContext;
|
|
||||||
export 'src/chart/common/datum_details.dart'
|
|
||||||
show DatumDetails, DomainFormatter, MeasureFormatter;
|
|
||||||
export 'src/chart/common/processed_series.dart'
|
|
||||||
show ImmutableSeries, MutableSeries;
|
|
||||||
export 'src/chart/common/series_datum.dart' show SeriesDatum, SeriesDatumConfig;
|
|
||||||
export 'src/chart/common/selection_model/selection_model.dart'
|
|
||||||
show SelectionModel, SelectionModelType, SelectionModelListener;
|
|
||||||
export 'src/chart/common/series_renderer.dart'
|
|
||||||
show rendererIdKey, rendererKey, SeriesRenderer;
|
|
||||||
export 'src/chart/common/series_renderer_config.dart'
|
|
||||||
show RendererAttributeKey, SeriesRendererConfig;
|
|
||||||
export 'src/chart/layout/layout_config.dart' show LayoutConfig, MarginSpec;
|
|
||||||
export 'src/chart/layout/layout_view.dart'
|
|
||||||
show
|
|
||||||
LayoutPosition,
|
|
||||||
LayoutView,
|
|
||||||
LayoutViewConfig,
|
|
||||||
LayoutViewPaintOrder,
|
|
||||||
LayoutViewPositionOrder,
|
|
||||||
ViewMargin,
|
|
||||||
ViewMeasuredSizes;
|
|
||||||
export 'src/chart/line/line_chart.dart' show LineChart;
|
|
||||||
export 'src/chart/line/line_renderer.dart' show LineRenderer;
|
|
||||||
export 'src/chart/line/line_renderer_config.dart' show LineRendererConfig;
|
|
||||||
export 'src/chart/pie/arc_label_decorator.dart'
|
|
||||||
show ArcLabelDecorator, ArcLabelLeaderLineStyleSpec, ArcLabelPosition;
|
|
||||||
export 'src/chart/pie/arc_renderer.dart' show ArcRenderer;
|
|
||||||
export 'src/chart/pie/arc_renderer_config.dart' show ArcRendererConfig;
|
|
||||||
export 'src/chart/pie/pie_chart.dart' show PieChart;
|
|
||||||
export 'src/chart/scatter_plot/comparison_points_decorator.dart'
|
|
||||||
show ComparisonPointsDecorator;
|
|
||||||
export 'src/chart/scatter_plot/point_renderer.dart'
|
|
||||||
show
|
|
||||||
boundsLineRadiusPxKey,
|
|
||||||
boundsLineRadiusPxFnKey,
|
|
||||||
pointSymbolRendererFnKey,
|
|
||||||
pointSymbolRendererIdKey,
|
|
||||||
PointRenderer;
|
|
||||||
export 'src/chart/scatter_plot/point_renderer_config.dart'
|
|
||||||
show PointRendererConfig;
|
|
||||||
export 'src/chart/scatter_plot/point_renderer_decorator.dart'
|
|
||||||
show PointRendererDecorator;
|
|
||||||
export 'src/chart/scatter_plot/scatter_plot_chart.dart' show ScatterPlotChart;
|
|
||||||
export 'src/chart/scatter_plot/symbol_annotation_renderer.dart'
|
|
||||||
show SymbolAnnotationRenderer;
|
|
||||||
export 'src/chart/scatter_plot/symbol_annotation_renderer_config.dart'
|
|
||||||
show SymbolAnnotationRendererConfig;
|
|
||||||
export 'src/chart/time_series/time_series_chart.dart' show TimeSeriesChart;
|
|
||||||
export 'src/common/color.dart' show Color;
|
|
||||||
export 'src/common/date_time_factory.dart'
|
|
||||||
show DateTimeFactory, LocalDateTimeFactory, UTCDateTimeFactory;
|
|
||||||
export 'src/common/gesture_listener.dart' show GestureListener;
|
|
||||||
export 'src/common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
export 'src/common/line_style.dart' show LineStyle;
|
|
||||||
export 'src/common/material_palette.dart' show MaterialPalette;
|
|
||||||
export 'src/common/performance.dart' show Performance;
|
|
||||||
export 'src/common/proxy_gesture_listener.dart' show ProxyGestureListener;
|
|
||||||
export 'src/common/rtl_spec.dart' show AxisDirection, RTLSpec;
|
|
||||||
export 'src/common/style/material_style.dart' show MaterialStyle;
|
|
||||||
export 'src/common/style/style_factory.dart' show StyleFactory;
|
|
||||||
export 'src/common/symbol_renderer.dart'
|
|
||||||
show
|
|
||||||
CircleSymbolRenderer,
|
|
||||||
CylinderSymbolRenderer,
|
|
||||||
LineSymbolRenderer,
|
|
||||||
PointSymbolRenderer,
|
|
||||||
RectSymbolRenderer,
|
|
||||||
RoundedRectSymbolRenderer,
|
|
||||||
SymbolRenderer;
|
|
||||||
export 'src/common/text_element.dart'
|
|
||||||
show TextElement, TextDirection, MaxWidthStrategy;
|
|
||||||
export 'src/common/text_measurement.dart' show TextMeasurement;
|
|
||||||
export 'src/common/text_style.dart' show TextStyle;
|
|
||||||
export 'src/data/series.dart' show Series, TypedAccessorFn;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:collection' show LinkedHashMap;
|
|
||||||
|
|
||||||
import '../bar/bar_renderer.dart' show BarRenderer;
|
|
||||||
import '../cartesian/axis/axis.dart' show NumericAxis;
|
|
||||||
import '../cartesian/cartesian_chart.dart' show OrdinalCartesianChart;
|
|
||||||
import '../common/series_renderer.dart' show SeriesRenderer;
|
|
||||||
import '../layout/layout_config.dart' show LayoutConfig;
|
|
||||||
|
|
||||||
class BarChart extends OrdinalCartesianChart {
|
|
||||||
BarChart(
|
|
||||||
{bool vertical,
|
|
||||||
LayoutConfig layoutConfig,
|
|
||||||
NumericAxis primaryMeasureAxis,
|
|
||||||
NumericAxis secondaryMeasureAxis,
|
|
||||||
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
|
|
||||||
: super(
|
|
||||||
vertical: vertical,
|
|
||||||
layoutConfig: layoutConfig,
|
|
||||||
primaryMeasureAxis: primaryMeasureAxis,
|
|
||||||
secondaryMeasureAxis: secondaryMeasureAxis,
|
|
||||||
disjointMeasureAxes: disjointMeasureAxes);
|
|
||||||
|
|
||||||
@override
|
|
||||||
SeriesRenderer<String> makeDefaultRenderer() {
|
|
||||||
return BarRenderer<String>()..rendererId = SeriesRenderer.defaultRendererId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show Rectangle;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../common/color.dart' show Color;
|
|
||||||
import '../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../common/text_element.dart' show TextDirection;
|
|
||||||
import '../../common/text_style.dart' show TextStyle;
|
|
||||||
import '../../data/series.dart' show AccessorFn;
|
|
||||||
import '../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
|
|
||||||
import '../common/chart_canvas.dart' show ChartCanvas;
|
|
||||||
import 'bar_renderer.dart' show ImmutableBarRendererElement;
|
|
||||||
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
|
|
||||||
|
|
||||||
class BarLabelDecorator<D> extends BarRendererDecorator<D> {
|
|
||||||
// Default configuration
|
|
||||||
static const _defaultLabelPosition = BarLabelPosition.auto;
|
|
||||||
static const _defaultLabelPadding = 5;
|
|
||||||
static const _defaultLabelAnchor = BarLabelAnchor.start;
|
|
||||||
static final _defaultInsideLabelStyle =
|
|
||||||
TextStyleSpec(fontSize: 12, color: Color.white);
|
|
||||||
static final _defaultOutsideLabelStyle =
|
|
||||||
TextStyleSpec(fontSize: 12, color: Color.black);
|
|
||||||
|
|
||||||
/// Configures [TextStyleSpec] for labels placed inside the bars.
|
|
||||||
final TextStyleSpec insideLabelStyleSpec;
|
|
||||||
|
|
||||||
/// Configures [TextStyleSpec] for labels placed outside the bars.
|
|
||||||
final TextStyleSpec outsideLabelStyleSpec;
|
|
||||||
|
|
||||||
/// Configures where to place the label relative to the bars.
|
|
||||||
final BarLabelPosition labelPosition;
|
|
||||||
|
|
||||||
/// For labels drawn inside the bar, configures label anchor position.
|
|
||||||
final BarLabelAnchor labelAnchor;
|
|
||||||
|
|
||||||
/// Space before and after the label text.
|
|
||||||
final int labelPadding;
|
|
||||||
|
|
||||||
BarLabelDecorator(
|
|
||||||
{TextStyleSpec insideLabelStyleSpec,
|
|
||||||
TextStyleSpec outsideLabelStyleSpec,
|
|
||||||
this.labelPosition = _defaultLabelPosition,
|
|
||||||
this.labelPadding = _defaultLabelPadding,
|
|
||||||
this.labelAnchor = _defaultLabelAnchor})
|
|
||||||
: insideLabelStyleSpec = insideLabelStyleSpec ?? _defaultInsideLabelStyle,
|
|
||||||
outsideLabelStyleSpec =
|
|
||||||
outsideLabelStyleSpec ?? _defaultOutsideLabelStyle;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void decorate(Iterable<ImmutableBarRendererElement<D>> barElements,
|
|
||||||
ChartCanvas canvas, GraphicsFactory graphicsFactory,
|
|
||||||
{@required Rectangle drawBounds,
|
|
||||||
@required double animationPercent,
|
|
||||||
@required bool renderingVertically,
|
|
||||||
bool rtl = false}) {
|
|
||||||
// TODO: Decorator not yet available for vertical charts.
|
|
||||||
assert(renderingVertically == false);
|
|
||||||
|
|
||||||
// Only decorate the bars when animation is at 100%.
|
|
||||||
if (animationPercent != 1.0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
|
|
||||||
// The [GraphicsFactory] is needed so it can't be created earlier.
|
|
||||||
final insideLabelStyle =
|
|
||||||
_getTextStyle(graphicsFactory, insideLabelStyleSpec);
|
|
||||||
final outsideLabelStyle =
|
|
||||||
_getTextStyle(graphicsFactory, outsideLabelStyleSpec);
|
|
||||||
|
|
||||||
for (var element in barElements) {
|
|
||||||
final labelFn = element.series.labelAccessorFn;
|
|
||||||
final datumIndex = element.index;
|
|
||||||
final label = (labelFn != null) ? labelFn(datumIndex) : null;
|
|
||||||
|
|
||||||
// If there are custom styles, use that instead of the default or the
|
|
||||||
// style defined for the entire decorator.
|
|
||||||
final datumInsideLabelStyle = _getDatumStyle(
|
|
||||||
element.series.insideLabelStyleAccessorFn,
|
|
||||||
datumIndex,
|
|
||||||
graphicsFactory,
|
|
||||||
defaultStyle: insideLabelStyle);
|
|
||||||
final datumOutsideLabelStyle = _getDatumStyle(
|
|
||||||
element.series.outsideLabelStyleAccessorFn,
|
|
||||||
datumIndex,
|
|
||||||
graphicsFactory,
|
|
||||||
defaultStyle: outsideLabelStyle);
|
|
||||||
|
|
||||||
// Skip calculation and drawing for this element if no label.
|
|
||||||
if (label == null || label.isEmpty) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final bounds = element.bounds;
|
|
||||||
|
|
||||||
// Get space available inside and outside the bar.
|
|
||||||
final totalPadding = labelPadding * 2;
|
|
||||||
final insideBarWidth = bounds.width - totalPadding;
|
|
||||||
final outsideBarWidth = drawBounds.width - bounds.width - totalPadding;
|
|
||||||
|
|
||||||
final labelElement = graphicsFactory.createTextElement(label);
|
|
||||||
var calculatedLabelPosition = labelPosition;
|
|
||||||
if (calculatedLabelPosition == BarLabelPosition.auto) {
|
|
||||||
// For auto, first try to fit the text inside the bar.
|
|
||||||
labelElement.textStyle = datumInsideLabelStyle;
|
|
||||||
|
|
||||||
// A label fits if the space inside the bar is >= outside bar or if the
|
|
||||||
// length of the text fits and the space. This is because if the bar has
|
|
||||||
// more space than the outside, it makes more sense to place the label
|
|
||||||
// inside the bar, even if the entire label does not fit.
|
|
||||||
calculatedLabelPosition = (insideBarWidth >= outsideBarWidth ||
|
|
||||||
labelElement.measurement.horizontalSliceWidth < insideBarWidth)
|
|
||||||
? BarLabelPosition.inside
|
|
||||||
: BarLabelPosition.outside;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the max width and text style.
|
|
||||||
if (calculatedLabelPosition == BarLabelPosition.inside) {
|
|
||||||
labelElement.textStyle = datumInsideLabelStyle;
|
|
||||||
labelElement.maxWidth = insideBarWidth;
|
|
||||||
} else {
|
|
||||||
// calculatedLabelPosition == LabelPosition.outside
|
|
||||||
labelElement.textStyle = datumOutsideLabelStyle;
|
|
||||||
labelElement.maxWidth = outsideBarWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only calculate and draw label if there's actually space for the label.
|
|
||||||
if (labelElement.maxWidth > 0) {
|
|
||||||
// Calculate the start position of label based on [labelAnchor].
|
|
||||||
int labelX;
|
|
||||||
if (calculatedLabelPosition == BarLabelPosition.inside) {
|
|
||||||
switch (labelAnchor) {
|
|
||||||
case BarLabelAnchor.middle:
|
|
||||||
labelX = (bounds.left +
|
|
||||||
bounds.width / 2 -
|
|
||||||
labelElement.measurement.horizontalSliceWidth / 2)
|
|
||||||
.round();
|
|
||||||
labelElement.textDirection =
|
|
||||||
rtl ? TextDirection.rtl : TextDirection.ltr;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BarLabelAnchor.end:
|
|
||||||
case BarLabelAnchor.start:
|
|
||||||
final alignLeft = rtl
|
|
||||||
? (labelAnchor == BarLabelAnchor.end)
|
|
||||||
: (labelAnchor == BarLabelAnchor.start);
|
|
||||||
|
|
||||||
if (alignLeft) {
|
|
||||||
labelX = bounds.left + labelPadding;
|
|
||||||
labelElement.textDirection = TextDirection.ltr;
|
|
||||||
} else {
|
|
||||||
labelX = bounds.right - labelPadding;
|
|
||||||
labelElement.textDirection = TextDirection.rtl;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// calculatedLabelPosition == LabelPosition.outside
|
|
||||||
labelX = bounds.right + labelPadding;
|
|
||||||
labelElement.textDirection = TextDirection.ltr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center the label inside the bar.
|
|
||||||
final labelY = (bounds.top +
|
|
||||||
(bounds.bottom - bounds.top) / 2 -
|
|
||||||
labelElement.measurement.verticalSliceWidth / 2)
|
|
||||||
.round();
|
|
||||||
|
|
||||||
canvas.drawText(labelElement, labelX, labelY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function that converts [TextStyleSpec] to [TextStyle].
|
|
||||||
TextStyle _getTextStyle(
|
|
||||||
GraphicsFactory graphicsFactory, TextStyleSpec labelSpec) {
|
|
||||||
return graphicsFactory.createTextPaint()
|
|
||||||
..color = labelSpec?.color ?? Color.black
|
|
||||||
..fontFamily = labelSpec?.fontFamily
|
|
||||||
..fontSize = labelSpec?.fontSize ?? 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper function to get datum specific style
|
|
||||||
TextStyle _getDatumStyle(AccessorFn<TextStyleSpec> labelFn, int datumIndex,
|
|
||||||
GraphicsFactory graphicsFactory,
|
|
||||||
{TextStyle defaultStyle}) {
|
|
||||||
final styleSpec = (labelFn != null) ? labelFn(datumIndex) : null;
|
|
||||||
return (styleSpec != null)
|
|
||||||
? _getTextStyle(graphicsFactory, styleSpec)
|
|
||||||
: defaultStyle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configures where to place the label relative to the bars.
|
|
||||||
enum BarLabelPosition {
|
|
||||||
/// Automatically try to place the label inside the bar first and place it on
|
|
||||||
/// the outside of the space available outside the bar is greater than space
|
|
||||||
/// available inside the bar.
|
|
||||||
auto,
|
|
||||||
|
|
||||||
/// Always place label on the outside.
|
|
||||||
outside,
|
|
||||||
|
|
||||||
/// Always place label on the inside.
|
|
||||||
inside,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configures where to anchor the label for labels drawn inside the bars.
|
|
||||||
enum BarLabelAnchor {
|
|
||||||
/// Anchor to the measure start.
|
|
||||||
start,
|
|
||||||
|
|
||||||
/// Anchor to the middle of the measure range.
|
|
||||||
middle,
|
|
||||||
|
|
||||||
/// Anchor to the measure end.
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:collection' show LinkedHashMap;
|
|
||||||
|
|
||||||
import '../../data/series.dart' show AttributeKey;
|
|
||||||
import '../cartesian/axis/axis.dart'
|
|
||||||
show ImmutableAxis, domainAxisKey, measureAxisKey;
|
|
||||||
import '../cartesian/cartesian_chart.dart' show CartesianChart;
|
|
||||||
import '../common/chart_canvas.dart' show ChartCanvas;
|
|
||||||
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
|
|
||||||
import 'bar_lane_renderer_config.dart' show BarLaneRendererConfig;
|
|
||||||
import 'bar_renderer.dart' show AnimatedBar, BarRenderer, BarRendererElement;
|
|
||||||
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
|
|
||||||
import 'base_bar_renderer.dart'
|
|
||||||
show
|
|
||||||
barGroupCountKey,
|
|
||||||
barGroupIndexKey,
|
|
||||||
barGroupWeightKey,
|
|
||||||
previousBarGroupWeightKey,
|
|
||||||
stackKeyKey;
|
|
||||||
import 'base_bar_renderer_element.dart' show BaseBarRendererElement;
|
|
||||||
|
|
||||||
/// Key for storing a list of all domain values that exist in the series data.
|
|
||||||
///
|
|
||||||
/// In grouped stacked mode, this list will contain a combination of domain
|
|
||||||
/// value and series category.
|
|
||||||
const domainValuesKey = AttributeKey<Set>('BarLaneRenderer.domainValues');
|
|
||||||
|
|
||||||
/// Renders series data as a series of bars with lanes.
|
|
||||||
///
|
|
||||||
/// Every stack of bars will have a swim lane rendered underneath the series
|
|
||||||
/// data, in a gray color by default. The swim lane occupies the same width as
|
|
||||||
/// the bar elements, and will be completely covered up if the bar stack happens
|
|
||||||
/// to take up the entire measure domain range.
|
|
||||||
///
|
|
||||||
/// If every bar that shares a domain value has a null measure value, then the
|
|
||||||
/// swim lanes may optionally be merged together into one wide lane that covers
|
|
||||||
/// the full domain range band width.
|
|
||||||
class BarLaneRenderer<D> extends BarRenderer<D> {
|
|
||||||
final BarRendererDecorator barRendererDecorator;
|
|
||||||
|
|
||||||
/// Store a map of domain+barGroupIndex+category index to bar lanes in a
|
|
||||||
/// stack.
|
|
||||||
///
|
|
||||||
/// This map is used to render all the bars in a stack together, to account
|
|
||||||
/// for rendering effects that need to take the full stack into account (e.g.
|
|
||||||
/// corner rounding).
|
|
||||||
///
|
|
||||||
/// [LinkedHashMap] is used to render the bars on the canvas in the same order
|
|
||||||
/// as the data was given to the chart. For the case where both grouping and
|
|
||||||
/// stacking are disabled, this means that bars for data later in the series
|
|
||||||
/// will be drawn "on top of" bars earlier in the series.
|
|
||||||
final _barLaneStackMap = LinkedHashMap<String, List<AnimatedBar<D>>>();
|
|
||||||
|
|
||||||
/// Store a map of flags to track whether all measure values for a given
|
|
||||||
/// domain value are null, for every series on the chart.
|
|
||||||
final _allMeasuresForDomainNullMap = LinkedHashMap<D, bool>();
|
|
||||||
|
|
||||||
factory BarLaneRenderer({BarLaneRendererConfig config, String rendererId}) {
|
|
||||||
rendererId ??= 'bar';
|
|
||||||
config ??= BarLaneRendererConfig();
|
|
||||||
return BarLaneRenderer._internal(config: config, rendererId: rendererId);
|
|
||||||
}
|
|
||||||
|
|
||||||
BarLaneRenderer._internal({BarLaneRendererConfig config, String rendererId})
|
|
||||||
: barRendererDecorator = config.barRendererDecorator,
|
|
||||||
super.internal(config: config, rendererId: rendererId);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void preprocessSeries(List<MutableSeries<D>> seriesList) {
|
|
||||||
super.preprocessSeries(seriesList);
|
|
||||||
|
|
||||||
_allMeasuresForDomainNullMap.clear();
|
|
||||||
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
final domainFn = series.domainFn;
|
|
||||||
final measureFn = series.rawMeasureFn;
|
|
||||||
|
|
||||||
final domainValues = Set<D>();
|
|
||||||
|
|
||||||
for (var barIndex = 0; barIndex < series.data.length; barIndex++) {
|
|
||||||
final domain = domainFn(barIndex);
|
|
||||||
final measure = measureFn(barIndex);
|
|
||||||
|
|
||||||
domainValues.add(domain);
|
|
||||||
|
|
||||||
// Update the "all measure null" tracking for bars that have the
|
|
||||||
// current domain value.
|
|
||||||
if ((config as BarLaneRendererConfig).mergeEmptyLanes) {
|
|
||||||
final allNull = _allMeasuresForDomainNullMap[domain];
|
|
||||||
final isNull = measure == null;
|
|
||||||
|
|
||||||
_allMeasuresForDomainNullMap[domain] =
|
|
||||||
allNull != null ? allNull && isNull : isNull;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
series.setAttr(domainValuesKey, domainValues);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void update(List<ImmutableSeries<D>> seriesList, bool isAnimatingThisDraw) {
|
|
||||||
super.update(seriesList, isAnimatingThisDraw);
|
|
||||||
|
|
||||||
// Add gray bars to render under every bar stack.
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
Set<D> domainValues = series.getAttr(domainValuesKey) as Set<D>;
|
|
||||||
|
|
||||||
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
|
|
||||||
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
|
|
||||||
final seriesStackKey = series.getAttr(stackKeyKey);
|
|
||||||
final barGroupCount = series.getAttr(barGroupCountKey);
|
|
||||||
final barGroupIndex = series.getAttr(barGroupIndexKey);
|
|
||||||
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
|
|
||||||
final barGroupWeight = series.getAttr(barGroupWeightKey);
|
|
||||||
final measureAxisPosition = measureAxis.getLocation(0.0);
|
|
||||||
final maxMeasureValue = _getMaxMeasureValue(measureAxis);
|
|
||||||
|
|
||||||
// Create a fake series for [BarLabelDecorator] to use when looking up the
|
|
||||||
// index of each datum.
|
|
||||||
final laneSeries = MutableSeries<D>.clone(seriesList[0]);
|
|
||||||
laneSeries.data = [];
|
|
||||||
|
|
||||||
// Don't render any labels on the swim lanes.
|
|
||||||
laneSeries.labelAccessorFn = (index) => '';
|
|
||||||
|
|
||||||
var laneSeriesIndex = 0;
|
|
||||||
domainValues.forEach((domainValue) {
|
|
||||||
// Skip adding any background bars if they will be covered up by the
|
|
||||||
// domain-spanning null bar.
|
|
||||||
if (_allMeasuresForDomainNullMap[domainValue] == true) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a fake datum to the series for [BarLabelDecorator].
|
|
||||||
final datum = {'index': laneSeriesIndex};
|
|
||||||
laneSeries.data.add(datum);
|
|
||||||
|
|
||||||
// Each bar should be stored in barStackMap in a structure that mirrors
|
|
||||||
// the visual rendering of the bars. Thus, they should be grouped by
|
|
||||||
// domain value, series category (by way of the stack keys that were
|
|
||||||
// generated for each series in the preprocess step), and bar group
|
|
||||||
// index to account for all combinations of grouping and stacking.
|
|
||||||
final barStackMapKey = domainValue.toString() +
|
|
||||||
'__' +
|
|
||||||
seriesStackKey +
|
|
||||||
'__' +
|
|
||||||
barGroupIndex.toString();
|
|
||||||
|
|
||||||
final barKey = barStackMapKey + '0';
|
|
||||||
|
|
||||||
final barStackList = _barLaneStackMap.putIfAbsent(
|
|
||||||
barStackMapKey, () => <AnimatedBar<D>>[]);
|
|
||||||
|
|
||||||
// If we already have an AnimatingBar for that index, use it.
|
|
||||||
var animatingBar = barStackList.firstWhere((bar) => bar.key == barKey,
|
|
||||||
orElse: () => null);
|
|
||||||
|
|
||||||
// If we don't have any existing bar element, create a new bar and have
|
|
||||||
// it animate in from the domain axis.
|
|
||||||
if (animatingBar == null) {
|
|
||||||
animatingBar = makeAnimatedBar(
|
|
||||||
key: barKey,
|
|
||||||
series: laneSeries,
|
|
||||||
datum: datum,
|
|
||||||
barGroupIndex: barGroupIndex,
|
|
||||||
previousBarGroupWeight: previousBarGroupWeight,
|
|
||||||
barGroupWeight: barGroupWeight,
|
|
||||||
color: (config as BarLaneRendererConfig).backgroundBarColor,
|
|
||||||
details: BarRendererElement<D>(),
|
|
||||||
domainValue: domainValue,
|
|
||||||
domainAxis: domainAxis,
|
|
||||||
domainWidth: domainAxis.rangeBand.round(),
|
|
||||||
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
|
|
||||||
measureValue: maxMeasureValue,
|
|
||||||
measureOffsetValue: 0.0,
|
|
||||||
measureAxisPosition: measureAxisPosition,
|
|
||||||
measureAxis: measureAxis,
|
|
||||||
numBarGroups: barGroupCount,
|
|
||||||
strokeWidthPx: config.strokeWidthPx,
|
|
||||||
measureIsNull: false,
|
|
||||||
measureIsNegative: false);
|
|
||||||
|
|
||||||
barStackList.add(animatingBar);
|
|
||||||
} else {
|
|
||||||
animatingBar
|
|
||||||
..datum = datum
|
|
||||||
..series = laneSeries
|
|
||||||
..domainValue = domainValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the barElement we are going to setup.
|
|
||||||
// Optimization to prevent allocation in non-animating case.
|
|
||||||
BaseBarRendererElement barElement = makeBarRendererElement(
|
|
||||||
barGroupIndex: barGroupIndex,
|
|
||||||
previousBarGroupWeight: previousBarGroupWeight,
|
|
||||||
barGroupWeight: barGroupWeight,
|
|
||||||
color: (config as BarLaneRendererConfig).backgroundBarColor,
|
|
||||||
details: BarRendererElement<D>(),
|
|
||||||
domainValue: domainValue,
|
|
||||||
domainAxis: domainAxis,
|
|
||||||
domainWidth: domainAxis.rangeBand.round(),
|
|
||||||
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
|
|
||||||
measureValue: maxMeasureValue,
|
|
||||||
measureOffsetValue: 0.0,
|
|
||||||
measureAxisPosition: measureAxisPosition,
|
|
||||||
measureAxis: measureAxis,
|
|
||||||
numBarGroups: barGroupCount,
|
|
||||||
strokeWidthPx: config.strokeWidthPx,
|
|
||||||
measureIsNull: false,
|
|
||||||
measureIsNegative: false);
|
|
||||||
|
|
||||||
animatingBar.setNewTarget(barElement);
|
|
||||||
|
|
||||||
laneSeriesIndex++;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add domain-spanning bars to render when every measure value for every
|
|
||||||
// datum of a given domain is null.
|
|
||||||
if ((config as BarLaneRendererConfig).mergeEmptyLanes) {
|
|
||||||
// Use the axes from the first series.
|
|
||||||
final domainAxis =
|
|
||||||
seriesList[0].getAttr(domainAxisKey) as ImmutableAxis<D>;
|
|
||||||
final measureAxis =
|
|
||||||
seriesList[0].getAttr(measureAxisKey) as ImmutableAxis<num>;
|
|
||||||
|
|
||||||
final measureAxisPosition = measureAxis.getLocation(0.0);
|
|
||||||
final maxMeasureValue = _getMaxMeasureValue(measureAxis);
|
|
||||||
|
|
||||||
final barGroupIndex = 0;
|
|
||||||
final previousBarGroupWeight = 0.0;
|
|
||||||
final barGroupWeight = 1.0;
|
|
||||||
final barGroupCount = 1;
|
|
||||||
|
|
||||||
// Create a fake series for [BarLabelDecorator] to use when looking up the
|
|
||||||
// index of each datum. We don't care about any other series values for
|
|
||||||
// the merged lanes, so just clone the first series.
|
|
||||||
final mergedSeries = MutableSeries<D>.clone(seriesList[0]);
|
|
||||||
mergedSeries.data = [];
|
|
||||||
|
|
||||||
// Add a label accessor that returns the empty lane label.
|
|
||||||
mergedSeries.labelAccessorFn =
|
|
||||||
(index) => (config as BarLaneRendererConfig).emptyLaneLabel;
|
|
||||||
|
|
||||||
var mergedSeriesIndex = 0;
|
|
||||||
_allMeasuresForDomainNullMap.forEach((domainValue, allNull) {
|
|
||||||
if (allNull) {
|
|
||||||
// Add a fake datum to the series for [BarLabelDecorator].
|
|
||||||
final datum = {'index': mergedSeriesIndex};
|
|
||||||
mergedSeries.data.add(datum);
|
|
||||||
|
|
||||||
final barStackMapKey = domainValue.toString() + '__allNull__';
|
|
||||||
|
|
||||||
final barKey = barStackMapKey + '0';
|
|
||||||
|
|
||||||
final barStackList = _barLaneStackMap.putIfAbsent(
|
|
||||||
barStackMapKey, () => <AnimatedBar<D>>[]);
|
|
||||||
|
|
||||||
// If we already have an AnimatingBar for that index, use it.
|
|
||||||
var animatingBar = barStackList.firstWhere((bar) => bar.key == barKey,
|
|
||||||
orElse: () => null);
|
|
||||||
|
|
||||||
// If we don't have any existing bar element, create a new bar and have
|
|
||||||
// it animate in from the domain axis.
|
|
||||||
if (animatingBar == null) {
|
|
||||||
animatingBar = makeAnimatedBar(
|
|
||||||
key: barKey,
|
|
||||||
series: mergedSeries,
|
|
||||||
datum: datum,
|
|
||||||
barGroupIndex: barGroupIndex,
|
|
||||||
previousBarGroupWeight: previousBarGroupWeight,
|
|
||||||
barGroupWeight: barGroupWeight,
|
|
||||||
color: (config as BarLaneRendererConfig).backgroundBarColor,
|
|
||||||
details: BarRendererElement<D>(),
|
|
||||||
domainValue: domainValue,
|
|
||||||
domainAxis: domainAxis,
|
|
||||||
domainWidth: domainAxis.rangeBand.round(),
|
|
||||||
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
|
|
||||||
measureValue: maxMeasureValue,
|
|
||||||
measureOffsetValue: 0.0,
|
|
||||||
measureAxisPosition: measureAxisPosition,
|
|
||||||
measureAxis: measureAxis,
|
|
||||||
numBarGroups: barGroupCount,
|
|
||||||
strokeWidthPx: config.strokeWidthPx,
|
|
||||||
measureIsNull: false,
|
|
||||||
measureIsNegative: false);
|
|
||||||
|
|
||||||
barStackList.add(animatingBar);
|
|
||||||
} else {
|
|
||||||
animatingBar
|
|
||||||
..datum = datum
|
|
||||||
..series = mergedSeries
|
|
||||||
..domainValue = domainValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the barElement we are going to setup.
|
|
||||||
// Optimization to prevent allocation in non-animating case.
|
|
||||||
BaseBarRendererElement barElement = makeBarRendererElement(
|
|
||||||
barGroupIndex: barGroupIndex,
|
|
||||||
previousBarGroupWeight: previousBarGroupWeight,
|
|
||||||
barGroupWeight: barGroupWeight,
|
|
||||||
color: (config as BarLaneRendererConfig).backgroundBarColor,
|
|
||||||
details: BarRendererElement<D>(),
|
|
||||||
domainValue: domainValue,
|
|
||||||
domainAxis: domainAxis,
|
|
||||||
domainWidth: domainAxis.rangeBand.round(),
|
|
||||||
fillColor: (config as BarLaneRendererConfig).backgroundBarColor,
|
|
||||||
measureValue: maxMeasureValue,
|
|
||||||
measureOffsetValue: 0.0,
|
|
||||||
measureAxisPosition: measureAxisPosition,
|
|
||||||
measureAxis: measureAxis,
|
|
||||||
numBarGroups: barGroupCount,
|
|
||||||
strokeWidthPx: config.strokeWidthPx,
|
|
||||||
measureIsNull: false,
|
|
||||||
measureIsNegative: false);
|
|
||||||
|
|
||||||
animatingBar.setNewTarget(barElement);
|
|
||||||
|
|
||||||
mergedSeriesIndex++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the maximum measure value that will fit in the draw area.
|
|
||||||
num _getMaxMeasureValue(ImmutableAxis<num> measureAxis) {
|
|
||||||
final pos = (chart as CartesianChart).vertical
|
|
||||||
? chart.drawAreaBounds.top
|
|
||||||
: isRtl ? chart.drawAreaBounds.left : chart.drawAreaBounds.right;
|
|
||||||
|
|
||||||
return measureAxis.getDomain(pos.toDouble());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Paints the current bar data on the canvas.
|
|
||||||
@override
|
|
||||||
void paint(ChartCanvas canvas, double animationPercent) {
|
|
||||||
_barLaneStackMap.forEach((stackKey, barStack) {
|
|
||||||
// Turn this into a list so that the getCurrentBar isn't called more than
|
|
||||||
// once for each animationPercent if the barElements are iterated more
|
|
||||||
// than once.
|
|
||||||
List<BarRendererElement<D>> barElements = barStack
|
|
||||||
.map((animatingBar) => animatingBar.getCurrentBar(animationPercent))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
paintBar(canvas, animationPercent, barElements);
|
|
||||||
});
|
|
||||||
|
|
||||||
super.paint(canvas, animationPercent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../common/color.dart' show Color;
|
|
||||||
import '../../common/style/style_factory.dart' show StyleFactory;
|
|
||||||
import '../../common/symbol_renderer.dart';
|
|
||||||
import '../common/chart_canvas.dart' show FillPatternType;
|
|
||||||
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
|
|
||||||
import 'bar_label_decorator.dart' show BarLabelDecorator;
|
|
||||||
import 'bar_lane_renderer.dart' show BarLaneRenderer;
|
|
||||||
import 'bar_renderer_config.dart' show BarRendererConfig, CornerStrategy;
|
|
||||||
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
|
|
||||||
import 'base_bar_renderer_config.dart' show BarGroupingType;
|
|
||||||
|
|
||||||
/// Configuration for a bar lane renderer.
|
|
||||||
class BarLaneRendererConfig extends BarRendererConfig<String> {
|
|
||||||
/// The color of background bars.
|
|
||||||
final Color backgroundBarColor;
|
|
||||||
|
|
||||||
/// Label text to draw on a merged empty lane.
|
|
||||||
///
|
|
||||||
/// This will only be drawn if all of the measures for a domain are null, and
|
|
||||||
/// [mergeEmptyLanes] is enabled.
|
|
||||||
///
|
|
||||||
/// The renderer must be configured with a [BarLabelDecorator] for this label
|
|
||||||
/// to be drawn.
|
|
||||||
final String emptyLaneLabel;
|
|
||||||
|
|
||||||
/// Whether or not all lanes for a given domain value should be merged into
|
|
||||||
/// one wide lane if all measure values for said domain are null.
|
|
||||||
final bool mergeEmptyLanes;
|
|
||||||
|
|
||||||
BarLaneRendererConfig({
|
|
||||||
String customRendererId,
|
|
||||||
CornerStrategy cornerStrategy,
|
|
||||||
this.emptyLaneLabel = 'No data',
|
|
||||||
FillPatternType fillPattern,
|
|
||||||
BarGroupingType groupingType,
|
|
||||||
int layoutPaintOrder = LayoutViewPaintOrder.bar,
|
|
||||||
this.mergeEmptyLanes = false,
|
|
||||||
int minBarLengthPx = 0,
|
|
||||||
double stackHorizontalSeparator,
|
|
||||||
double strokeWidthPx = 0.0,
|
|
||||||
BarRendererDecorator barRendererDecorator,
|
|
||||||
SymbolRenderer symbolRenderer,
|
|
||||||
Color backgroundBarColor,
|
|
||||||
List<int> weightPattern,
|
|
||||||
}) : backgroundBarColor =
|
|
||||||
backgroundBarColor ?? StyleFactory.style.noDataColor,
|
|
||||||
super(
|
|
||||||
barRendererDecorator: barRendererDecorator,
|
|
||||||
cornerStrategy: cornerStrategy,
|
|
||||||
customRendererId: customRendererId,
|
|
||||||
groupingType: groupingType ?? BarGroupingType.grouped,
|
|
||||||
layoutPaintOrder: layoutPaintOrder,
|
|
||||||
minBarLengthPx: minBarLengthPx,
|
|
||||||
fillPattern: fillPattern,
|
|
||||||
stackHorizontalSeparator: stackHorizontalSeparator,
|
|
||||||
strokeWidthPx: strokeWidthPx,
|
|
||||||
symbolRenderer: symbolRenderer,
|
|
||||||
weightPattern: weightPattern,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
BarLaneRenderer<String> build() {
|
|
||||||
return BarLaneRenderer<String>(config: this, rendererId: customRendererId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) {
|
|
||||||
if (identical(this, other)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!(other is BarLaneRendererConfig)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return other.backgroundBarColor == backgroundBarColor &&
|
|
||||||
other.emptyLaneLabel == emptyLaneLabel &&
|
|
||||||
other.mergeEmptyLanes == mergeEmptyLanes &&
|
|
||||||
super == (other);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
var hash = super.hashCode;
|
|
||||||
hash = hash * 31 + (backgroundBarColor?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (emptyLaneLabel?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (mergeEmptyLanes?.hashCode ?? 0);
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,556 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show max, min, Point, Rectangle;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show protected, required;
|
|
||||||
|
|
||||||
import '../../common/color.dart' show Color;
|
|
||||||
import '../cartesian/axis/axis.dart'
|
|
||||||
show ImmutableAxis, domainAxisKey, measureAxisKey;
|
|
||||||
import '../common/canvas_shapes.dart' show CanvasBarStack, CanvasRect;
|
|
||||||
import '../common/chart_canvas.dart' show ChartCanvas, FillPatternType;
|
|
||||||
import '../common/datum_details.dart' show DatumDetails;
|
|
||||||
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
|
|
||||||
import '../common/series_datum.dart' show SeriesDatum;
|
|
||||||
import 'bar_renderer_config.dart' show BarRendererConfig, CornerStrategy;
|
|
||||||
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
|
|
||||||
import 'base_bar_renderer.dart'
|
|
||||||
show
|
|
||||||
BaseBarRenderer,
|
|
||||||
barGroupCountKey,
|
|
||||||
barGroupIndexKey,
|
|
||||||
previousBarGroupWeightKey,
|
|
||||||
barGroupWeightKey;
|
|
||||||
import 'base_bar_renderer_element.dart'
|
|
||||||
show BaseAnimatedBar, BaseBarRendererElement;
|
|
||||||
|
|
||||||
/// Renders series data as a series of bars.
|
|
||||||
class BarRenderer<D>
|
|
||||||
extends BaseBarRenderer<D, BarRendererElement<D>, AnimatedBar<D>> {
|
|
||||||
/// If we are grouped, use this spacing between the bars in a group.
|
|
||||||
final _barGroupInnerPadding = 2;
|
|
||||||
|
|
||||||
/// The padding between bar stacks.
|
|
||||||
///
|
|
||||||
/// The padding comes out of the bottom of the bar.
|
|
||||||
final _stackedBarPadding = 1;
|
|
||||||
|
|
||||||
final BarRendererDecorator barRendererDecorator;
|
|
||||||
|
|
||||||
factory BarRenderer({BarRendererConfig config, String rendererId}) {
|
|
||||||
rendererId ??= 'bar';
|
|
||||||
config ??= BarRendererConfig();
|
|
||||||
return BarRenderer.internal(config: config, rendererId: rendererId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This constructor is protected because it is used by child classes, which
|
|
||||||
/// cannot call the factory in their own constructors.
|
|
||||||
@protected
|
|
||||||
BarRenderer.internal({BarRendererConfig config, String rendererId})
|
|
||||||
: barRendererDecorator = config.barRendererDecorator,
|
|
||||||
super(
|
|
||||||
config: config,
|
|
||||||
rendererId: rendererId,
|
|
||||||
layoutPaintOrder: config.layoutPaintOrder);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void configureSeries(List<MutableSeries<D>> seriesList) {
|
|
||||||
assignMissingColors(getOrderedSeriesList(seriesList),
|
|
||||||
emptyCategoryUsesSinglePalette: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
DatumDetails<D> addPositionToDetailsForSeriesDatum(
|
|
||||||
DatumDetails<D> details, SeriesDatum<D> seriesDatum) {
|
|
||||||
final series = details.series;
|
|
||||||
|
|
||||||
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
|
|
||||||
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
|
|
||||||
|
|
||||||
final barGroupIndex = series.getAttr(barGroupIndexKey);
|
|
||||||
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
|
|
||||||
final barGroupWeight = series.getAttr(barGroupWeightKey);
|
|
||||||
final numBarGroups = series.getAttr(barGroupCountKey);
|
|
||||||
|
|
||||||
final bounds = _getBarBounds(
|
|
||||||
details.domain,
|
|
||||||
domainAxis,
|
|
||||||
domainAxis.rangeBand.round(),
|
|
||||||
details.measure,
|
|
||||||
details.measureOffset,
|
|
||||||
measureAxis,
|
|
||||||
barGroupIndex,
|
|
||||||
previousBarGroupWeight,
|
|
||||||
barGroupWeight,
|
|
||||||
numBarGroups);
|
|
||||||
|
|
||||||
Point<double> chartPosition;
|
|
||||||
|
|
||||||
if (renderingVertically) {
|
|
||||||
chartPosition = Point<double>(
|
|
||||||
(bounds.left + (bounds.width / 2)).toDouble(), bounds.top.toDouble());
|
|
||||||
} else {
|
|
||||||
chartPosition = Point<double>(
|
|
||||||
isRtl ? bounds.left.toDouble() : bounds.right.toDouble(),
|
|
||||||
(bounds.top + (bounds.height / 2)).toDouble());
|
|
||||||
}
|
|
||||||
|
|
||||||
return DatumDetails.from(details, chartPosition: chartPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
BarRendererElement<D> getBaseDetails(dynamic datum, int index) {
|
|
||||||
return BarRendererElement<D>();
|
|
||||||
}
|
|
||||||
|
|
||||||
CornerStrategy get cornerStrategy {
|
|
||||||
return (config as BarRendererConfig).cornerStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates an [AnimatedBar] to represent the previous and current state
|
|
||||||
/// of one bar on the chart.
|
|
||||||
@override
|
|
||||||
AnimatedBar<D> makeAnimatedBar(
|
|
||||||
{String key,
|
|
||||||
ImmutableSeries<D> series,
|
|
||||||
List<int> dashPattern,
|
|
||||||
dynamic datum,
|
|
||||||
Color color,
|
|
||||||
BarRendererElement<D> details,
|
|
||||||
D domainValue,
|
|
||||||
ImmutableAxis<D> domainAxis,
|
|
||||||
int domainWidth,
|
|
||||||
num measureValue,
|
|
||||||
num measureOffsetValue,
|
|
||||||
ImmutableAxis<num> measureAxis,
|
|
||||||
double measureAxisPosition,
|
|
||||||
Color fillColor,
|
|
||||||
FillPatternType fillPattern,
|
|
||||||
double strokeWidthPx,
|
|
||||||
int barGroupIndex,
|
|
||||||
double previousBarGroupWeight,
|
|
||||||
double barGroupWeight,
|
|
||||||
int numBarGroups,
|
|
||||||
bool measureIsNull,
|
|
||||||
bool measureIsNegative}) {
|
|
||||||
return AnimatedBar<D>(
|
|
||||||
key: key, datum: datum, series: series, domainValue: domainValue)
|
|
||||||
..setNewTarget(makeBarRendererElement(
|
|
||||||
color: color,
|
|
||||||
dashPattern: dashPattern,
|
|
||||||
details: details,
|
|
||||||
domainValue: domainValue,
|
|
||||||
domainAxis: domainAxis,
|
|
||||||
domainWidth: domainWidth,
|
|
||||||
measureValue: measureValue,
|
|
||||||
measureOffsetValue: measureOffsetValue,
|
|
||||||
measureAxisPosition: measureAxisPosition,
|
|
||||||
measureAxis: measureAxis,
|
|
||||||
fillColor: fillColor,
|
|
||||||
fillPattern: fillPattern,
|
|
||||||
strokeWidthPx: strokeWidthPx,
|
|
||||||
barGroupIndex: barGroupIndex,
|
|
||||||
previousBarGroupWeight: previousBarGroupWeight,
|
|
||||||
barGroupWeight: barGroupWeight,
|
|
||||||
numBarGroups: numBarGroups,
|
|
||||||
measureIsNull: measureIsNull,
|
|
||||||
measureIsNegative: measureIsNegative));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a [BarRendererElement] to represent the rendering data for one
|
|
||||||
/// bar on the chart.
|
|
||||||
@override
|
|
||||||
BarRendererElement<D> makeBarRendererElement(
|
|
||||||
{Color color,
|
|
||||||
List<int> dashPattern,
|
|
||||||
BarRendererElement<D> details,
|
|
||||||
D domainValue,
|
|
||||||
ImmutableAxis<D> domainAxis,
|
|
||||||
int domainWidth,
|
|
||||||
num measureValue,
|
|
||||||
num measureOffsetValue,
|
|
||||||
ImmutableAxis<num> measureAxis,
|
|
||||||
double measureAxisPosition,
|
|
||||||
Color fillColor,
|
|
||||||
FillPatternType fillPattern,
|
|
||||||
double strokeWidthPx,
|
|
||||||
int barGroupIndex,
|
|
||||||
double previousBarGroupWeight,
|
|
||||||
double barGroupWeight,
|
|
||||||
int numBarGroups,
|
|
||||||
bool measureIsNull,
|
|
||||||
bool measureIsNegative}) {
|
|
||||||
return BarRendererElement<D>()
|
|
||||||
..color = color
|
|
||||||
..dashPattern = dashPattern
|
|
||||||
..fillColor = fillColor
|
|
||||||
..fillPattern = fillPattern
|
|
||||||
..measureAxisPosition = measureAxisPosition
|
|
||||||
..roundPx = details.roundPx
|
|
||||||
..strokeWidthPx = strokeWidthPx
|
|
||||||
..measureIsNull = measureIsNull
|
|
||||||
..measureIsNegative = measureIsNegative
|
|
||||||
..bounds = _getBarBounds(
|
|
||||||
domainValue,
|
|
||||||
domainAxis,
|
|
||||||
domainWidth,
|
|
||||||
measureValue,
|
|
||||||
measureOffsetValue,
|
|
||||||
measureAxis,
|
|
||||||
barGroupIndex,
|
|
||||||
previousBarGroupWeight,
|
|
||||||
barGroupWeight,
|
|
||||||
numBarGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paintBar(ChartCanvas canvas, double animationPercent,
|
|
||||||
Iterable<BarRendererElement<D>> barElements) {
|
|
||||||
final bars = <CanvasRect>[];
|
|
||||||
|
|
||||||
// When adjusting bars for stacked bar padding, do not modify the first bar
|
|
||||||
// if rendering vertically and do not modify the last bar if rendering
|
|
||||||
// horizontally.
|
|
||||||
final unmodifiedBar =
|
|
||||||
renderingVertically ? barElements.first : barElements.last;
|
|
||||||
|
|
||||||
// Find the max bar width from each segment to calculate corner radius.
|
|
||||||
int maxBarWidth = 0;
|
|
||||||
|
|
||||||
var measureIsNegative = false;
|
|
||||||
|
|
||||||
for (var bar in barElements) {
|
|
||||||
var bounds = bar.bounds;
|
|
||||||
|
|
||||||
measureIsNegative = measureIsNegative || bar.measureIsNegative;
|
|
||||||
|
|
||||||
if (bar != unmodifiedBar) {
|
|
||||||
bounds = renderingVertically
|
|
||||||
? Rectangle<int>(
|
|
||||||
bar.bounds.left,
|
|
||||||
max(
|
|
||||||
0,
|
|
||||||
bar.bounds.top +
|
|
||||||
(measureIsNegative ? _stackedBarPadding : 0)),
|
|
||||||
bar.bounds.width,
|
|
||||||
max(0, bar.bounds.height - _stackedBarPadding),
|
|
||||||
)
|
|
||||||
: Rectangle<int>(
|
|
||||||
max(
|
|
||||||
0,
|
|
||||||
bar.bounds.left +
|
|
||||||
(measureIsNegative ? _stackedBarPadding : 0)),
|
|
||||||
bar.bounds.top,
|
|
||||||
max(0, bar.bounds.width - _stackedBarPadding),
|
|
||||||
bar.bounds.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bars.add(CanvasRect(bounds,
|
|
||||||
dashPattern: bar.dashPattern,
|
|
||||||
fill: bar.fillColor,
|
|
||||||
pattern: bar.fillPattern,
|
|
||||||
stroke: bar.color,
|
|
||||||
strokeWidthPx: bar.strokeWidthPx));
|
|
||||||
|
|
||||||
maxBarWidth = max(
|
|
||||||
maxBarWidth, (renderingVertically ? bounds.width : bounds.height));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool roundTopLeft;
|
|
||||||
bool roundTopRight;
|
|
||||||
bool roundBottomLeft;
|
|
||||||
bool roundBottomRight;
|
|
||||||
|
|
||||||
if (measureIsNegative) {
|
|
||||||
// Negative bars should be rounded towards the negative axis direction.
|
|
||||||
// In vertical mode, this is the bottom. In horizontal mode, this is the
|
|
||||||
// left side of the chart for LTR, or the right side for RTL.
|
|
||||||
roundTopLeft = !renderingVertically && !isRtl ? true : false;
|
|
||||||
roundTopRight = !renderingVertically && isRtl ? true : false;
|
|
||||||
roundBottomLeft = renderingVertically || !isRtl ? true : false;
|
|
||||||
roundBottomRight = renderingVertically || isRtl ? true : false;
|
|
||||||
} else {
|
|
||||||
// Positive bars should be rounded towards the positive axis direction.
|
|
||||||
// In vertical mode, this is the top. In horizontal mode, this is the
|
|
||||||
// right side of the chart for LTR, or the left side for RTL.
|
|
||||||
roundTopLeft = renderingVertically || isRtl ? true : false;
|
|
||||||
roundTopRight = isRtl ? false : true;
|
|
||||||
roundBottomLeft = isRtl ? true : false;
|
|
||||||
roundBottomRight = renderingVertically || isRtl ? false : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final barStack = CanvasBarStack(
|
|
||||||
bars,
|
|
||||||
radius: cornerStrategy.getRadius(maxBarWidth),
|
|
||||||
stackedBarPadding: _stackedBarPadding,
|
|
||||||
roundTopLeft: roundTopLeft,
|
|
||||||
roundTopRight: roundTopRight,
|
|
||||||
roundBottomLeft: roundBottomLeft,
|
|
||||||
roundBottomRight: roundBottomRight,
|
|
||||||
);
|
|
||||||
|
|
||||||
// If bar stack's range width is:
|
|
||||||
// * Within the component bounds, then draw the bar stack.
|
|
||||||
// * Partially out of component bounds, then clip the stack where it is out
|
|
||||||
// of bounds.
|
|
||||||
// * Fully out of component bounds, do not draw.
|
|
||||||
|
|
||||||
final barOutsideBounds = renderingVertically
|
|
||||||
? barStack.fullStackRect.left < componentBounds.left ||
|
|
||||||
barStack.fullStackRect.right > componentBounds.right
|
|
||||||
: barStack.fullStackRect.top < componentBounds.top ||
|
|
||||||
barStack.fullStackRect.bottom > componentBounds.bottom;
|
|
||||||
|
|
||||||
// TODO: When we have initial viewport, add image test for
|
|
||||||
// clipping.
|
|
||||||
if (barOutsideBounds) {
|
|
||||||
final clipBounds = _getBarStackBounds(barStack.fullStackRect);
|
|
||||||
|
|
||||||
// Do not draw the bar stack if it is completely outside of the component
|
|
||||||
// bounds.
|
|
||||||
if (clipBounds.width <= 0 || clipBounds.height <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.setClipBounds(clipBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawBarStack(barStack, drawAreaBounds: componentBounds);
|
|
||||||
|
|
||||||
if (barOutsideBounds) {
|
|
||||||
canvas.resetClipBounds();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decorate the bar segments if there is a decorator.
|
|
||||||
barRendererDecorator?.decorate(barElements, canvas, graphicsFactory,
|
|
||||||
drawBounds: drawBounds,
|
|
||||||
animationPercent: animationPercent,
|
|
||||||
renderingVertically: renderingVertically,
|
|
||||||
rtl: isRtl);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate the clipping region for a rectangle that represents the full bar
|
|
||||||
/// stack.
|
|
||||||
Rectangle<int> _getBarStackBounds(Rectangle<int> barStackRect) {
|
|
||||||
int left;
|
|
||||||
int right;
|
|
||||||
int top;
|
|
||||||
int bottom;
|
|
||||||
|
|
||||||
if (renderingVertically) {
|
|
||||||
// Only clip at the start and end so that the bar's width stays within
|
|
||||||
// the viewport, but any bar decorations above the bar can still show.
|
|
||||||
left = max(componentBounds.left, barStackRect.left);
|
|
||||||
right = min(componentBounds.right, barStackRect.right);
|
|
||||||
top = barStackRect.top;
|
|
||||||
bottom = barStackRect.bottom;
|
|
||||||
} else {
|
|
||||||
// Only clip at the top and bottom so that the bar's height stays within
|
|
||||||
// the viewport, but any bar decorations to the right of the bar can still
|
|
||||||
// show.
|
|
||||||
left = barStackRect.left;
|
|
||||||
right = barStackRect.right;
|
|
||||||
top = max(componentBounds.top, barStackRect.top);
|
|
||||||
bottom = min(componentBounds.bottom, barStackRect.bottom);
|
|
||||||
}
|
|
||||||
|
|
||||||
final width = right - left;
|
|
||||||
final height = bottom - top;
|
|
||||||
|
|
||||||
return Rectangle(left, top, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a set of bounds that describe a bar.
|
|
||||||
Rectangle<int> _getBarBounds(
|
|
||||||
D domainValue,
|
|
||||||
ImmutableAxis<D> domainAxis,
|
|
||||||
int domainWidth,
|
|
||||||
num measureValue,
|
|
||||||
num measureOffsetValue,
|
|
||||||
ImmutableAxis<num> measureAxis,
|
|
||||||
int barGroupIndex,
|
|
||||||
double previousBarGroupWeight,
|
|
||||||
double barGroupWeight,
|
|
||||||
int numBarGroups) {
|
|
||||||
// If no weights were passed in, default to equal weight per bar.
|
|
||||||
if (barGroupWeight == null) {
|
|
||||||
barGroupWeight = 1 / numBarGroups;
|
|
||||||
previousBarGroupWeight = barGroupIndex * barGroupWeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate how wide each bar should be within the group of bars. If we
|
|
||||||
// only have one series, or are stacked, then barWidth should equal
|
|
||||||
// domainWidth.
|
|
||||||
int spacingLoss = (_barGroupInnerPadding * (numBarGroups - 1));
|
|
||||||
int barWidth = ((domainWidth - spacingLoss) * barGroupWeight).round();
|
|
||||||
|
|
||||||
// Make sure that bars are at least one pixel wide, so that they will always
|
|
||||||
// be visible on the chart. Ideally we should do something clever with the
|
|
||||||
// size of the chart, and the density and periodicity of the data, but this
|
|
||||||
// at least ensures that dense charts still have visible data.
|
|
||||||
barWidth = max(1, barWidth);
|
|
||||||
|
|
||||||
// Flip bar group index for calculating location on the domain axis if RTL.
|
|
||||||
final adjustedBarGroupIndex =
|
|
||||||
isRtl ? numBarGroups - barGroupIndex - 1 : barGroupIndex;
|
|
||||||
|
|
||||||
// Calculate the start and end of the bar, taking into account accumulated
|
|
||||||
// padding for grouped bars.
|
|
||||||
int previousAverageWidth = adjustedBarGroupIndex > 0
|
|
||||||
? ((domainWidth - spacingLoss) *
|
|
||||||
(previousBarGroupWeight / adjustedBarGroupIndex))
|
|
||||||
.round()
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
int domainStart = (domainAxis.getLocation(domainValue) -
|
|
||||||
(domainWidth / 2) +
|
|
||||||
(previousAverageWidth + _barGroupInnerPadding) *
|
|
||||||
adjustedBarGroupIndex)
|
|
||||||
.round();
|
|
||||||
|
|
||||||
int domainEnd = domainStart + barWidth;
|
|
||||||
|
|
||||||
measureValue = measureValue != null ? measureValue : 0;
|
|
||||||
|
|
||||||
// Calculate measure locations. Stacked bars should have their
|
|
||||||
// offset calculated previously.
|
|
||||||
int measureStart;
|
|
||||||
int measureEnd;
|
|
||||||
if (measureValue < 0) {
|
|
||||||
measureEnd = measureAxis.getLocation(measureOffsetValue).round();
|
|
||||||
measureStart =
|
|
||||||
measureAxis.getLocation(measureValue + measureOffsetValue).round();
|
|
||||||
} else {
|
|
||||||
measureStart = measureAxis.getLocation(measureOffsetValue).round();
|
|
||||||
measureEnd =
|
|
||||||
measureAxis.getLocation(measureValue + measureOffsetValue).round();
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle<int> bounds;
|
|
||||||
if (this.renderingVertically) {
|
|
||||||
// Rectangle clamps to zero width/height
|
|
||||||
bounds = Rectangle<int>(domainStart, measureEnd, domainEnd - domainStart,
|
|
||||||
measureStart - measureEnd);
|
|
||||||
} else {
|
|
||||||
// Rectangle clamps to zero width/height
|
|
||||||
bounds = Rectangle<int>(min(measureStart, measureEnd), domainStart,
|
|
||||||
(measureEnd - measureStart).abs(), domainEnd - domainStart);
|
|
||||||
}
|
|
||||||
return bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Rectangle<int> getBoundsForBar(BarRendererElement bar) => bar.bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class ImmutableBarRendererElement<D> {
|
|
||||||
ImmutableSeries<D> get series;
|
|
||||||
|
|
||||||
dynamic get datum;
|
|
||||||
|
|
||||||
int get index;
|
|
||||||
|
|
||||||
Rectangle<int> get bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
class BarRendererElement<D> extends BaseBarRendererElement
|
|
||||||
implements ImmutableBarRendererElement<D> {
|
|
||||||
ImmutableSeries<D> series;
|
|
||||||
Rectangle<int> bounds;
|
|
||||||
int roundPx;
|
|
||||||
int index;
|
|
||||||
dynamic _datum;
|
|
||||||
|
|
||||||
dynamic get datum => _datum;
|
|
||||||
|
|
||||||
set datum(dynamic datum) {
|
|
||||||
_datum = datum;
|
|
||||||
index = series?.data?.indexOf(datum);
|
|
||||||
}
|
|
||||||
|
|
||||||
BarRendererElement();
|
|
||||||
|
|
||||||
BarRendererElement.clone(BarRendererElement other) : super.clone(other) {
|
|
||||||
series = other.series;
|
|
||||||
bounds = other.bounds;
|
|
||||||
roundPx = other.roundPx;
|
|
||||||
index = other.index;
|
|
||||||
_datum = other._datum;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateAnimationPercent(BaseBarRendererElement previous,
|
|
||||||
BaseBarRendererElement target, double animationPercent) {
|
|
||||||
final BarRendererElement localPrevious = previous;
|
|
||||||
final BarRendererElement localTarget = target;
|
|
||||||
|
|
||||||
final previousBounds = localPrevious.bounds;
|
|
||||||
final targetBounds = localTarget.bounds;
|
|
||||||
|
|
||||||
var top = ((targetBounds.top - previousBounds.top) * animationPercent) +
|
|
||||||
previousBounds.top;
|
|
||||||
var right =
|
|
||||||
((targetBounds.right - previousBounds.right) * animationPercent) +
|
|
||||||
previousBounds.right;
|
|
||||||
var bottom =
|
|
||||||
((targetBounds.bottom - previousBounds.bottom) * animationPercent) +
|
|
||||||
previousBounds.bottom;
|
|
||||||
var left = ((targetBounds.left - previousBounds.left) * animationPercent) +
|
|
||||||
previousBounds.left;
|
|
||||||
|
|
||||||
bounds = Rectangle<int>(left.round(), top.round(), (right - left).round(),
|
|
||||||
(bottom - top).round());
|
|
||||||
|
|
||||||
roundPx = localTarget.roundPx;
|
|
||||||
|
|
||||||
super.updateAnimationPercent(previous, target, animationPercent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnimatedBar<D> extends BaseAnimatedBar<D, BarRendererElement<D>> {
|
|
||||||
AnimatedBar(
|
|
||||||
{@required String key,
|
|
||||||
@required dynamic datum,
|
|
||||||
@required ImmutableSeries<D> series,
|
|
||||||
@required D domainValue})
|
|
||||||
: super(key: key, datum: datum, series: series, domainValue: domainValue);
|
|
||||||
|
|
||||||
@override
|
|
||||||
animateElementToMeasureAxisPosition(BaseBarRendererElement target) {
|
|
||||||
final BarRendererElement localTarget = target;
|
|
||||||
|
|
||||||
// TODO: Animate out bars in the middle of a stack.
|
|
||||||
localTarget.bounds = Rectangle<int>(
|
|
||||||
localTarget.bounds.left + (localTarget.bounds.width / 2).round(),
|
|
||||||
localTarget.measureAxisPosition.round(),
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
|
|
||||||
BarRendererElement<D> getCurrentBar(double animationPercent) {
|
|
||||||
final BarRendererElement<D> bar = super.getCurrentBar(animationPercent);
|
|
||||||
|
|
||||||
// Update with series and datum information to pass to bar decorator.
|
|
||||||
bar.series = series;
|
|
||||||
bar.datum = datum;
|
|
||||||
|
|
||||||
return bar;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
BarRendererElement<D> clone(BarRendererElement bar) =>
|
|
||||||
BarRendererElement<D>.clone(bar);
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../common/symbol_renderer.dart';
|
|
||||||
import '../common/chart_canvas.dart' show FillPatternType;
|
|
||||||
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
|
|
||||||
import 'bar_renderer.dart' show BarRenderer;
|
|
||||||
import 'bar_renderer_decorator.dart' show BarRendererDecorator;
|
|
||||||
import 'base_bar_renderer_config.dart'
|
|
||||||
show BarGroupingType, BaseBarRendererConfig;
|
|
||||||
|
|
||||||
/// Configuration for a bar renderer.
|
|
||||||
class BarRendererConfig<D> extends BaseBarRendererConfig<D> {
|
|
||||||
/// Strategy for determining the corner radius of a bar.
|
|
||||||
final CornerStrategy cornerStrategy;
|
|
||||||
|
|
||||||
/// Decorator for optionally decorating painted bars.
|
|
||||||
final BarRendererDecorator barRendererDecorator;
|
|
||||||
|
|
||||||
BarRendererConfig({
|
|
||||||
String customRendererId,
|
|
||||||
CornerStrategy cornerStrategy,
|
|
||||||
FillPatternType fillPattern,
|
|
||||||
BarGroupingType groupingType,
|
|
||||||
int layoutPaintOrder = LayoutViewPaintOrder.bar,
|
|
||||||
int minBarLengthPx = 0,
|
|
||||||
double stackHorizontalSeparator,
|
|
||||||
double strokeWidthPx = 0.0,
|
|
||||||
this.barRendererDecorator,
|
|
||||||
SymbolRenderer symbolRenderer,
|
|
||||||
List<int> weightPattern,
|
|
||||||
}) : cornerStrategy = cornerStrategy ?? const ConstCornerStrategy(2),
|
|
||||||
super(
|
|
||||||
customRendererId: customRendererId,
|
|
||||||
groupingType: groupingType ?? BarGroupingType.grouped,
|
|
||||||
layoutPaintOrder: layoutPaintOrder,
|
|
||||||
minBarLengthPx: minBarLengthPx,
|
|
||||||
fillPattern: fillPattern,
|
|
||||||
stackHorizontalSeparator: stackHorizontalSeparator,
|
|
||||||
strokeWidthPx: strokeWidthPx,
|
|
||||||
symbolRenderer: symbolRenderer,
|
|
||||||
weightPattern: weightPattern,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
BarRenderer<D> build() {
|
|
||||||
return BarRenderer<D>(config: this, rendererId: customRendererId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) {
|
|
||||||
if (identical(this, other)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!(other is BarRendererConfig)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return other.cornerStrategy == cornerStrategy && super == (other);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
var hash = super.hashCode;
|
|
||||||
hash = hash * 31 + (cornerStrategy?.hashCode ?? 0);
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class CornerStrategy {
|
|
||||||
/// Returns the radius of the rounded corners in pixels.
|
|
||||||
int getRadius(int barWidth);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Strategy for constant corner radius.
|
|
||||||
class ConstCornerStrategy implements CornerStrategy {
|
|
||||||
final int radius;
|
|
||||||
|
|
||||||
const ConstCornerStrategy(this.radius);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int getRadius(_) => radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Strategy for no corner radius.
|
|
||||||
class NoCornerStrategy extends ConstCornerStrategy {
|
|
||||||
const NoCornerStrategy() : super(0);
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show Rectangle;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../common/chart_canvas.dart' show ChartCanvas;
|
|
||||||
import 'bar_renderer.dart' show ImmutableBarRendererElement;
|
|
||||||
|
|
||||||
/// Decorates bars after the bars have already been painted.
|
|
||||||
abstract class BarRendererDecorator<D> {
|
|
||||||
const BarRendererDecorator();
|
|
||||||
|
|
||||||
void decorate(Iterable<ImmutableBarRendererElement<D>> barElements,
|
|
||||||
ChartCanvas canvas, GraphicsFactory graphicsFactory,
|
|
||||||
{@required Rectangle drawBounds,
|
|
||||||
@required double animationPercent,
|
|
||||||
@required bool renderingVertically,
|
|
||||||
bool rtl = false});
|
|
||||||
}
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show Point, Rectangle, max, min;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../common/color.dart' show Color;
|
|
||||||
import '../cartesian/axis/axis.dart'
|
|
||||||
show ImmutableAxis, domainAxisKey, measureAxisKey;
|
|
||||||
import '../common/chart_canvas.dart' show ChartCanvas, FillPatternType;
|
|
||||||
import '../common/datum_details.dart' show DatumDetails;
|
|
||||||
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
|
|
||||||
import '../common/series_datum.dart' show SeriesDatum;
|
|
||||||
import 'bar_target_line_renderer_config.dart' show BarTargetLineRendererConfig;
|
|
||||||
import 'base_bar_renderer.dart'
|
|
||||||
show
|
|
||||||
BaseBarRenderer,
|
|
||||||
barGroupCountKey,
|
|
||||||
barGroupIndexKey,
|
|
||||||
previousBarGroupWeightKey,
|
|
||||||
barGroupWeightKey;
|
|
||||||
import 'base_bar_renderer_element.dart'
|
|
||||||
show BaseAnimatedBar, BaseBarRendererElement;
|
|
||||||
|
|
||||||
/// Renders series data as a series of bar target lines.
|
|
||||||
///
|
|
||||||
/// Usually paired with a BarRenderer to display target metrics alongside actual
|
|
||||||
/// metrics.
|
|
||||||
class BarTargetLineRenderer<D> extends BaseBarRenderer<D,
|
|
||||||
_BarTargetLineRendererElement, _AnimatedBarTargetLine<D>> {
|
|
||||||
/// If we are grouped, use this spacing between the bars in a group.
|
|
||||||
final _barGroupInnerPadding = 2;
|
|
||||||
|
|
||||||
/// Standard color for all bar target lines.
|
|
||||||
final _color = Color(r: 0, g: 0, b: 0, a: 153);
|
|
||||||
|
|
||||||
factory BarTargetLineRenderer(
|
|
||||||
{BarTargetLineRendererConfig<D> config,
|
|
||||||
String rendererId = 'barTargetLine'}) {
|
|
||||||
config ??= BarTargetLineRendererConfig<D>();
|
|
||||||
return BarTargetLineRenderer._internal(
|
|
||||||
config: config, rendererId: rendererId);
|
|
||||||
}
|
|
||||||
|
|
||||||
BarTargetLineRenderer._internal(
|
|
||||||
{BarTargetLineRendererConfig<D> config, String rendererId})
|
|
||||||
: super(
|
|
||||||
config: config,
|
|
||||||
rendererId: rendererId,
|
|
||||||
layoutPaintOrder: config.layoutPaintOrder);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void configureSeries(List<MutableSeries<D>> seriesList) {
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
series.colorFn ??= (_) => _color;
|
|
||||||
series.fillColorFn ??= (_) => _color;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
DatumDetails<D> addPositionToDetailsForSeriesDatum(
|
|
||||||
DatumDetails<D> details, SeriesDatum<D> seriesDatum) {
|
|
||||||
final series = details.series;
|
|
||||||
|
|
||||||
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
|
|
||||||
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
|
|
||||||
|
|
||||||
final barGroupIndex = series.getAttr(barGroupIndexKey);
|
|
||||||
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
|
|
||||||
final barGroupWeight = series.getAttr(barGroupWeightKey);
|
|
||||||
final numBarGroups = series.getAttr(barGroupCountKey);
|
|
||||||
|
|
||||||
final points = _getTargetLinePoints(
|
|
||||||
details.domain,
|
|
||||||
domainAxis,
|
|
||||||
domainAxis.rangeBand.round(),
|
|
||||||
details.measure,
|
|
||||||
details.measureOffset,
|
|
||||||
measureAxis,
|
|
||||||
barGroupIndex,
|
|
||||||
previousBarGroupWeight,
|
|
||||||
barGroupWeight,
|
|
||||||
numBarGroups);
|
|
||||||
|
|
||||||
Point<double> chartPosition;
|
|
||||||
|
|
||||||
if (renderingVertically) {
|
|
||||||
chartPosition = Point<double>(
|
|
||||||
(points[0].x + (points[1].x - points[0].x) / 2).toDouble(),
|
|
||||||
points[0].y.toDouble());
|
|
||||||
} else {
|
|
||||||
chartPosition = Point<double>(points[0].x.toDouble(),
|
|
||||||
(points[0].y + (points[1].y - points[0].y) / 2).toDouble());
|
|
||||||
}
|
|
||||||
|
|
||||||
return DatumDetails.from(details, chartPosition: chartPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
_BarTargetLineRendererElement getBaseDetails(dynamic datum, int index) {
|
|
||||||
final BarTargetLineRendererConfig<D> localConfig = config;
|
|
||||||
return _BarTargetLineRendererElement()
|
|
||||||
..roundEndCaps = localConfig.roundEndCaps;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates an [_AnimatedBarTargetLine] to represent the previous and
|
|
||||||
/// current state of one bar target line on the chart.
|
|
||||||
@override
|
|
||||||
_AnimatedBarTargetLine<D> makeAnimatedBar(
|
|
||||||
{String key,
|
|
||||||
ImmutableSeries<D> series,
|
|
||||||
dynamic datum,
|
|
||||||
Color color,
|
|
||||||
List<int> dashPattern,
|
|
||||||
_BarTargetLineRendererElement details,
|
|
||||||
D domainValue,
|
|
||||||
ImmutableAxis<D> domainAxis,
|
|
||||||
int domainWidth,
|
|
||||||
num measureValue,
|
|
||||||
num measureOffsetValue,
|
|
||||||
ImmutableAxis<num> measureAxis,
|
|
||||||
double measureAxisPosition,
|
|
||||||
Color fillColor,
|
|
||||||
FillPatternType fillPattern,
|
|
||||||
int barGroupIndex,
|
|
||||||
double previousBarGroupWeight,
|
|
||||||
double barGroupWeight,
|
|
||||||
int numBarGroups,
|
|
||||||
double strokeWidthPx,
|
|
||||||
bool measureIsNull,
|
|
||||||
bool measureIsNegative}) {
|
|
||||||
return _AnimatedBarTargetLine(
|
|
||||||
key: key, datum: datum, series: series, domainValue: domainValue)
|
|
||||||
..setNewTarget(makeBarRendererElement(
|
|
||||||
color: color,
|
|
||||||
details: details,
|
|
||||||
dashPattern: dashPattern,
|
|
||||||
domainValue: domainValue,
|
|
||||||
domainAxis: domainAxis,
|
|
||||||
domainWidth: domainWidth,
|
|
||||||
measureValue: measureValue,
|
|
||||||
measureOffsetValue: measureOffsetValue,
|
|
||||||
measureAxisPosition: measureAxisPosition,
|
|
||||||
measureAxis: measureAxis,
|
|
||||||
fillColor: fillColor,
|
|
||||||
fillPattern: fillPattern,
|
|
||||||
strokeWidthPx: strokeWidthPx,
|
|
||||||
barGroupIndex: barGroupIndex,
|
|
||||||
previousBarGroupWeight: previousBarGroupWeight,
|
|
||||||
barGroupWeight: barGroupWeight,
|
|
||||||
numBarGroups: numBarGroups,
|
|
||||||
measureIsNull: measureIsNull,
|
|
||||||
measureIsNegative: measureIsNegative));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a [_BarTargetLineRendererElement] to represent the rendering
|
|
||||||
/// data for one bar target line on the chart.
|
|
||||||
@override
|
|
||||||
_BarTargetLineRendererElement makeBarRendererElement(
|
|
||||||
{Color color,
|
|
||||||
List<int> dashPattern,
|
|
||||||
_BarTargetLineRendererElement details,
|
|
||||||
D domainValue,
|
|
||||||
ImmutableAxis<D> domainAxis,
|
|
||||||
int domainWidth,
|
|
||||||
num measureValue,
|
|
||||||
num measureOffsetValue,
|
|
||||||
ImmutableAxis<num> measureAxis,
|
|
||||||
double measureAxisPosition,
|
|
||||||
Color fillColor,
|
|
||||||
FillPatternType fillPattern,
|
|
||||||
double strokeWidthPx,
|
|
||||||
int barGroupIndex,
|
|
||||||
double previousBarGroupWeight,
|
|
||||||
double barGroupWeight,
|
|
||||||
int numBarGroups,
|
|
||||||
bool measureIsNull,
|
|
||||||
bool measureIsNegative}) {
|
|
||||||
return _BarTargetLineRendererElement()
|
|
||||||
..color = color
|
|
||||||
..dashPattern = dashPattern
|
|
||||||
..fillColor = fillColor
|
|
||||||
..fillPattern = fillPattern
|
|
||||||
..measureAxisPosition = measureAxisPosition
|
|
||||||
..roundEndCaps = details.roundEndCaps
|
|
||||||
..strokeWidthPx = strokeWidthPx
|
|
||||||
..measureIsNull = measureIsNull
|
|
||||||
..measureIsNegative = measureIsNegative
|
|
||||||
..points = _getTargetLinePoints(
|
|
||||||
domainValue,
|
|
||||||
domainAxis,
|
|
||||||
domainWidth,
|
|
||||||
measureValue,
|
|
||||||
measureOffsetValue,
|
|
||||||
measureAxis,
|
|
||||||
barGroupIndex,
|
|
||||||
previousBarGroupWeight,
|
|
||||||
barGroupWeight,
|
|
||||||
numBarGroups);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paintBar(
|
|
||||||
ChartCanvas canvas,
|
|
||||||
double animationPercent,
|
|
||||||
Iterable<_BarTargetLineRendererElement> barElements,
|
|
||||||
) {
|
|
||||||
barElements.forEach((bar) {
|
|
||||||
// TODO: Combine common line attributes into
|
|
||||||
// GraphicsFactory.lineStyle or similar.
|
|
||||||
canvas.drawLine(
|
|
||||||
clipBounds: drawBounds,
|
|
||||||
points: bar.points,
|
|
||||||
stroke: bar.color,
|
|
||||||
roundEndCaps: bar.roundEndCaps,
|
|
||||||
strokeWidthPx: bar.strokeWidthPx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a set of points that describe a bar target line.
|
|
||||||
List<Point<int>> _getTargetLinePoints(
|
|
||||||
D domainValue,
|
|
||||||
ImmutableAxis<D> domainAxis,
|
|
||||||
int domainWidth,
|
|
||||||
num measureValue,
|
|
||||||
num measureOffsetValue,
|
|
||||||
ImmutableAxis<num> measureAxis,
|
|
||||||
int barGroupIndex,
|
|
||||||
double previousBarGroupWeight,
|
|
||||||
double barGroupWeight,
|
|
||||||
int numBarGroups) {
|
|
||||||
// If no weights were passed in, default to equal weight per bar.
|
|
||||||
if (barGroupWeight == null) {
|
|
||||||
barGroupWeight = 1 / numBarGroups;
|
|
||||||
previousBarGroupWeight = barGroupIndex * barGroupWeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
final BarTargetLineRendererConfig<D> localConfig = config;
|
|
||||||
|
|
||||||
// Calculate how wide each bar target line should be within the group of
|
|
||||||
// bar target lines. If we only have one series, or are stacked, then
|
|
||||||
// barWidth should equal domainWidth.
|
|
||||||
int spacingLoss = (_barGroupInnerPadding * (numBarGroups - 1));
|
|
||||||
int barWidth = ((domainWidth - spacingLoss) * barGroupWeight).round();
|
|
||||||
|
|
||||||
// Get the overdraw boundaries.
|
|
||||||
var overDrawOuterPx = localConfig.overDrawOuterPx;
|
|
||||||
var overDrawPx = localConfig.overDrawPx;
|
|
||||||
|
|
||||||
int overDrawStartPx = (barGroupIndex == 0) && overDrawOuterPx != null
|
|
||||||
? overDrawOuterPx
|
|
||||||
: overDrawPx;
|
|
||||||
|
|
||||||
int overDrawEndPx =
|
|
||||||
(barGroupIndex == numBarGroups - 1) && overDrawOuterPx != null
|
|
||||||
? overDrawOuterPx
|
|
||||||
: overDrawPx;
|
|
||||||
|
|
||||||
// Flip bar group index for calculating location on the domain axis if RTL.
|
|
||||||
final adjustedBarGroupIndex =
|
|
||||||
isRtl ? numBarGroups - barGroupIndex - 1 : barGroupIndex;
|
|
||||||
|
|
||||||
// Calculate the start and end of the bar target line, taking into account
|
|
||||||
// accumulated padding for grouped bars.
|
|
||||||
num previousAverageWidth = adjustedBarGroupIndex > 0
|
|
||||||
? ((domainWidth - spacingLoss) *
|
|
||||||
(previousBarGroupWeight / adjustedBarGroupIndex))
|
|
||||||
.round()
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
int domainStart = (domainAxis.getLocation(domainValue) -
|
|
||||||
(domainWidth / 2) +
|
|
||||||
(previousAverageWidth + _barGroupInnerPadding) *
|
|
||||||
adjustedBarGroupIndex -
|
|
||||||
overDrawStartPx)
|
|
||||||
.round();
|
|
||||||
|
|
||||||
int domainEnd = domainStart + barWidth + overDrawStartPx + overDrawEndPx;
|
|
||||||
|
|
||||||
measureValue = measureValue != null ? measureValue : 0;
|
|
||||||
|
|
||||||
// Calculate measure locations. Stacked bars should have their
|
|
||||||
// offset calculated previously.
|
|
||||||
int measureStart =
|
|
||||||
measureAxis.getLocation(measureValue + measureOffsetValue).round();
|
|
||||||
|
|
||||||
List<Point<int>> points;
|
|
||||||
if (renderingVertically) {
|
|
||||||
points = [
|
|
||||||
Point<int>(domainStart, measureStart),
|
|
||||||
Point<int>(domainEnd, measureStart)
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
points = [
|
|
||||||
Point<int>(measureStart, domainStart),
|
|
||||||
Point<int>(measureStart, domainEnd)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Rectangle<int> getBoundsForBar(_BarTargetLineRendererElement bar) {
|
|
||||||
final points = bar.points;
|
|
||||||
int top;
|
|
||||||
int bottom;
|
|
||||||
int left;
|
|
||||||
int right;
|
|
||||||
points.forEach((p) {
|
|
||||||
top = top != null ? min(top, p.y) : p.y;
|
|
||||||
left = left != null ? min(left, p.x) : p.x;
|
|
||||||
bottom = bottom != null ? max(bottom, p.y) : p.y;
|
|
||||||
right = right != null ? max(right, p.x) : p.x;
|
|
||||||
});
|
|
||||||
return Rectangle<int>(left, top, right - left, bottom - top);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BarTargetLineRendererElement extends BaseBarRendererElement {
|
|
||||||
List<Point<int>> points;
|
|
||||||
bool roundEndCaps;
|
|
||||||
|
|
||||||
_BarTargetLineRendererElement();
|
|
||||||
|
|
||||||
_BarTargetLineRendererElement.clone(_BarTargetLineRendererElement other)
|
|
||||||
: super.clone(other) {
|
|
||||||
points = List<Point<int>>.from(other.points);
|
|
||||||
roundEndCaps = other.roundEndCaps;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateAnimationPercent(BaseBarRendererElement previous,
|
|
||||||
BaseBarRendererElement target, double animationPercent) {
|
|
||||||
final _BarTargetLineRendererElement localPrevious = previous;
|
|
||||||
final _BarTargetLineRendererElement localTarget = target;
|
|
||||||
|
|
||||||
final previousPoints = localPrevious.points;
|
|
||||||
final targetPoints = localTarget.points;
|
|
||||||
|
|
||||||
Point<int> lastPoint;
|
|
||||||
|
|
||||||
int pointIndex;
|
|
||||||
for (pointIndex = 0; pointIndex < targetPoints.length; pointIndex++) {
|
|
||||||
var targetPoint = targetPoints[pointIndex];
|
|
||||||
|
|
||||||
// If we have more points than the previous line, animate in the new point
|
|
||||||
// by starting its measure position at the last known official point.
|
|
||||||
Point<int> previousPoint;
|
|
||||||
if (previousPoints.length - 1 >= pointIndex) {
|
|
||||||
previousPoint = previousPoints[pointIndex];
|
|
||||||
lastPoint = previousPoint;
|
|
||||||
} else {
|
|
||||||
previousPoint = Point<int>(targetPoint.x, lastPoint.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
var x = ((targetPoint.x - previousPoint.x) * animationPercent) +
|
|
||||||
previousPoint.x;
|
|
||||||
|
|
||||||
var y = ((targetPoint.y - previousPoint.y) * animationPercent) +
|
|
||||||
previousPoint.y;
|
|
||||||
|
|
||||||
if (points.length - 1 >= pointIndex) {
|
|
||||||
points[pointIndex] = Point<int>(x.round(), y.round());
|
|
||||||
} else {
|
|
||||||
points.add(Point<int>(x.round(), y.round()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removing extra points that don't exist anymore.
|
|
||||||
if (pointIndex < points.length) {
|
|
||||||
points.removeRange(pointIndex, points.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
strokeWidthPx = ((localTarget.strokeWidthPx - localPrevious.strokeWidthPx) *
|
|
||||||
animationPercent) +
|
|
||||||
localPrevious.strokeWidthPx;
|
|
||||||
|
|
||||||
roundEndCaps = localTarget.roundEndCaps;
|
|
||||||
|
|
||||||
super.updateAnimationPercent(previous, target, animationPercent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedBarTargetLine<D>
|
|
||||||
extends BaseAnimatedBar<D, _BarTargetLineRendererElement> {
|
|
||||||
_AnimatedBarTargetLine(
|
|
||||||
{@required String key,
|
|
||||||
@required dynamic datum,
|
|
||||||
@required ImmutableSeries<D> series,
|
|
||||||
@required D domainValue})
|
|
||||||
: super(key: key, datum: datum, series: series, domainValue: domainValue);
|
|
||||||
|
|
||||||
@override
|
|
||||||
animateElementToMeasureAxisPosition(BaseBarRendererElement target) {
|
|
||||||
final _BarTargetLineRendererElement localTarget = target;
|
|
||||||
|
|
||||||
final newPoints = <Point<int>>[];
|
|
||||||
for (var index = 0; index < localTarget.points.length; index++) {
|
|
||||||
final targetPoint = localTarget.points[index];
|
|
||||||
|
|
||||||
newPoints.add(
|
|
||||||
Point<int>(targetPoint.x, localTarget.measureAxisPosition.round()));
|
|
||||||
}
|
|
||||||
localTarget.points = newPoints;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
_BarTargetLineRendererElement clone(_BarTargetLineRendererElement bar) =>
|
|
||||||
_BarTargetLineRendererElement.clone(bar);
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../common/symbol_renderer.dart'
|
|
||||||
show SymbolRenderer, LineSymbolRenderer;
|
|
||||||
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
|
|
||||||
import 'bar_target_line_renderer.dart' show BarTargetLineRenderer;
|
|
||||||
import 'base_bar_renderer_config.dart'
|
|
||||||
show BarGroupingType, BaseBarRendererConfig;
|
|
||||||
|
|
||||||
/// Configuration for a bar target line renderer.
|
|
||||||
class BarTargetLineRendererConfig<D> extends BaseBarRendererConfig<D> {
|
|
||||||
/// The number of pixels that the line will extend beyond the bandwidth at the
|
|
||||||
/// edges of the bar group.
|
|
||||||
///
|
|
||||||
/// If set, this overrides overDrawPx for the beginning side of the first bar
|
|
||||||
/// target line in the group, and the ending side of the last bar target line.
|
|
||||||
/// overDrawPx will be used for overdrawing the target lines for interior
|
|
||||||
/// sides of the bars.
|
|
||||||
final int overDrawOuterPx;
|
|
||||||
|
|
||||||
/// The number of pixels that the line will extend beyond the bandwidth for
|
|
||||||
/// every bar in a group.
|
|
||||||
final int overDrawPx;
|
|
||||||
|
|
||||||
/// Whether target lines should have round end caps, or square if false.
|
|
||||||
final bool roundEndCaps;
|
|
||||||
|
|
||||||
BarTargetLineRendererConfig(
|
|
||||||
{String customRendererId,
|
|
||||||
List<int> dashPattern,
|
|
||||||
groupingType = BarGroupingType.grouped,
|
|
||||||
int layoutPaintOrder = LayoutViewPaintOrder.barTargetLine,
|
|
||||||
int minBarLengthPx = 0,
|
|
||||||
this.overDrawOuterPx,
|
|
||||||
this.overDrawPx = 0,
|
|
||||||
this.roundEndCaps = true,
|
|
||||||
double strokeWidthPx = 3.0,
|
|
||||||
SymbolRenderer symbolRenderer,
|
|
||||||
List<int> weightPattern})
|
|
||||||
: super(
|
|
||||||
customRendererId: customRendererId,
|
|
||||||
dashPattern: dashPattern,
|
|
||||||
groupingType: groupingType,
|
|
||||||
layoutPaintOrder: layoutPaintOrder,
|
|
||||||
minBarLengthPx: minBarLengthPx,
|
|
||||||
strokeWidthPx: strokeWidthPx,
|
|
||||||
symbolRenderer: symbolRenderer ?? LineSymbolRenderer(),
|
|
||||||
weightPattern: weightPattern,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
BarTargetLineRenderer<D> build() {
|
|
||||||
return BarTargetLineRenderer<D>(config: this, rendererId: customRendererId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) {
|
|
||||||
if (identical(this, other)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!(other is BarTargetLineRendererConfig)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return other.overDrawOuterPx == overDrawOuterPx &&
|
|
||||||
other.overDrawPx == overDrawPx &&
|
|
||||||
other.roundEndCaps == roundEndCaps &&
|
|
||||||
super == (other);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
var hash = 1;
|
|
||||||
hash = hash * 31 + (overDrawOuterPx?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (overDrawPx?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (roundEndCaps?.hashCode ?? 0);
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,797 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:collection' show LinkedHashMap, HashSet;
|
|
||||||
import 'dart:math' show Point, Rectangle, max;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show protected, required;
|
|
||||||
|
|
||||||
import '../../common/color.dart' show Color;
|
|
||||||
import '../../common/math.dart' show clamp;
|
|
||||||
import '../../common/symbol_renderer.dart' show RoundedRectSymbolRenderer;
|
|
||||||
import '../../data/series.dart' show AttributeKey;
|
|
||||||
import '../cartesian/axis/axis.dart'
|
|
||||||
show ImmutableAxis, OrdinalAxis, domainAxisKey, measureAxisKey;
|
|
||||||
import '../cartesian/axis/scale.dart' show RangeBandConfig;
|
|
||||||
import '../cartesian/cartesian_renderer.dart' show BaseCartesianRenderer;
|
|
||||||
import '../common/base_chart.dart' show BaseChart;
|
|
||||||
import '../common/chart_canvas.dart' show ChartCanvas, FillPatternType;
|
|
||||||
import '../common/datum_details.dart' show DatumDetails;
|
|
||||||
import '../common/processed_series.dart' show ImmutableSeries, MutableSeries;
|
|
||||||
import 'base_bar_renderer_config.dart' show BaseBarRendererConfig;
|
|
||||||
import 'base_bar_renderer_element.dart'
|
|
||||||
show BaseAnimatedBar, BaseBarRendererElement;
|
|
||||||
|
|
||||||
const barGroupIndexKey = AttributeKey<int>('BarRenderer.barGroupIndex');
|
|
||||||
|
|
||||||
const barGroupCountKey = AttributeKey<int>('BarRenderer.barGroupCount');
|
|
||||||
|
|
||||||
const barGroupWeightKey = AttributeKey<double>('BarRenderer.barGroupWeight');
|
|
||||||
|
|
||||||
const previousBarGroupWeightKey =
|
|
||||||
AttributeKey<double>('BarRenderer.previousBarGroupWeight');
|
|
||||||
|
|
||||||
const stackKeyKey = AttributeKey<String>('BarRenderer.stackKey');
|
|
||||||
|
|
||||||
const barElementsKey =
|
|
||||||
AttributeKey<List<BaseBarRendererElement>>('BarRenderer.elements');
|
|
||||||
|
|
||||||
/// Base class for bar renderers that implements common stacking and grouping
|
|
||||||
/// logic.
|
|
||||||
///
|
|
||||||
/// Bar renderers support 4 different modes of rendering multiple series on the
|
|
||||||
/// chart, configured by the grouped and stacked flags.
|
|
||||||
/// * grouped - Render bars for each series that shares a domain value
|
|
||||||
/// side-by-side.
|
|
||||||
/// * stacked - Render bars for each series that shares a domain value in a
|
|
||||||
/// stack, ordered in the same order as the series list.
|
|
||||||
/// * grouped-stacked: Render bars for each series that shares a domain value in
|
|
||||||
/// a group of bar stacks. Each stack will contain all the series that share a
|
|
||||||
/// series category.
|
|
||||||
/// * floating style - When grouped and stacked are both false, all bars that
|
|
||||||
/// share a domain value will be rendered in the same domain space. Each datum
|
|
||||||
/// should be configured with a measure offset to position its bar along the
|
|
||||||
/// measure axis. Bars will freely overlap if their measure values and measure
|
|
||||||
/// offsets overlap. Note that bars for each series will be rendered in order,
|
|
||||||
/// such that bars from the last series will be "on top" of bars from previous
|
|
||||||
/// series.
|
|
||||||
abstract class BaseBarRenderer<D, R extends BaseBarRendererElement,
|
|
||||||
B extends BaseAnimatedBar<D, R>> extends BaseCartesianRenderer<D> {
|
|
||||||
final BaseBarRendererConfig config;
|
|
||||||
|
|
||||||
@protected
|
|
||||||
BaseChart<D> chart;
|
|
||||||
|
|
||||||
/// Store a map of domain+barGroupIndex+category index to bars in a stack.
|
|
||||||
///
|
|
||||||
/// This map is used to render all the bars in a stack together, to account
|
|
||||||
/// for rendering effects that need to take the full stack into account (e.g.
|
|
||||||
/// corner rounding).
|
|
||||||
///
|
|
||||||
/// [LinkedHashMap] is used to render the bars on the canvas in the same order
|
|
||||||
/// as the data was given to the chart. For the case where both grouping and
|
|
||||||
/// stacking are disabled, this means that bars for data later in the series
|
|
||||||
/// will be drawn "on top of" bars earlier in the series.
|
|
||||||
final _barStackMap = LinkedHashMap<String, List<B>>();
|
|
||||||
|
|
||||||
// Store a list of bar stacks that exist in the series data.
|
|
||||||
//
|
|
||||||
// This list will be used to remove any AnimatingBars that were rendered in
|
|
||||||
// previous draw cycles, but no longer have a corresponding datum in the new
|
|
||||||
// data.
|
|
||||||
final _currentKeys = <String>[];
|
|
||||||
|
|
||||||
/// Stores a list of stack keys for each group key.
|
|
||||||
final _currentGroupsStackKeys = LinkedHashMap<D, Set<String>>();
|
|
||||||
|
|
||||||
/// Optimization for getNearest to avoid scanning all data if possible.
|
|
||||||
ImmutableAxis<D> _prevDomainAxis;
|
|
||||||
|
|
||||||
BaseBarRenderer(
|
|
||||||
{@required this.config, String rendererId, int layoutPaintOrder})
|
|
||||||
: super(
|
|
||||||
rendererId: rendererId,
|
|
||||||
layoutPaintOrder: layoutPaintOrder,
|
|
||||||
symbolRenderer: config?.symbolRenderer ?? RoundedRectSymbolRenderer(),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void preprocessSeries(List<MutableSeries<D>> seriesList) {
|
|
||||||
var barGroupIndex = 0;
|
|
||||||
|
|
||||||
// Maps used to store the final measure offset of the previous series, for
|
|
||||||
// each domain value.
|
|
||||||
final posDomainToStackKeyToDetailsMap = {};
|
|
||||||
final negDomainToStackKeyToDetailsMap = {};
|
|
||||||
final categoryToIndexMap = {};
|
|
||||||
|
|
||||||
// Keep track of the largest bar stack size. This should be 1 for grouped
|
|
||||||
// bars, and it should be the size of the tallest stack for stacked or
|
|
||||||
// grouped stacked bars.
|
|
||||||
var maxBarStackSize = 0;
|
|
||||||
|
|
||||||
final orderedSeriesList = getOrderedSeriesList(seriesList);
|
|
||||||
|
|
||||||
orderedSeriesList.forEach((series) {
|
|
||||||
var elements = <BaseBarRendererElement>[];
|
|
||||||
|
|
||||||
var domainFn = series.domainFn;
|
|
||||||
var measureFn = series.measureFn;
|
|
||||||
var measureOffsetFn = series.measureOffsetFn;
|
|
||||||
var fillPatternFn = series.fillPatternFn;
|
|
||||||
var strokeWidthPxFn = series.strokeWidthPxFn;
|
|
||||||
|
|
||||||
series.dashPatternFn ??= (_) => config.dashPattern;
|
|
||||||
|
|
||||||
// Identifies which stack the series will go in, by default a single
|
|
||||||
// stack.
|
|
||||||
var stackKey = '__defaultKey__';
|
|
||||||
|
|
||||||
// Override the stackKey with seriesCategory if we are GROUPED_STACKED
|
|
||||||
// so we have a way to choose which series go into which stacks.
|
|
||||||
if (config.grouped && config.stacked) {
|
|
||||||
if (series.seriesCategory != null) {
|
|
||||||
stackKey = series.seriesCategory;
|
|
||||||
}
|
|
||||||
|
|
||||||
barGroupIndex = categoryToIndexMap[stackKey];
|
|
||||||
if (barGroupIndex == null) {
|
|
||||||
barGroupIndex = categoryToIndexMap.length;
|
|
||||||
categoryToIndexMap[stackKey] = barGroupIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var needsMeasureOffset = false;
|
|
||||||
|
|
||||||
for (var barIndex = 0; barIndex < series.data.length; barIndex++) {
|
|
||||||
dynamic datum = series.data[barIndex];
|
|
||||||
final details = getBaseDetails(datum, barIndex);
|
|
||||||
|
|
||||||
details.barStackIndex = 0;
|
|
||||||
details.measureOffset = 0;
|
|
||||||
|
|
||||||
if (fillPatternFn != null) {
|
|
||||||
details.fillPattern = fillPatternFn(barIndex);
|
|
||||||
} else {
|
|
||||||
details.fillPattern = config.fillPattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strokeWidthPxFn != null) {
|
|
||||||
details.strokeWidthPx = strokeWidthPxFn(barIndex).toDouble();
|
|
||||||
} else {
|
|
||||||
details.strokeWidthPx = config.strokeWidthPx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When stacking is enabled, adjust the measure offset for each domain
|
|
||||||
// value in each series by adding up the measures and offsets of lower
|
|
||||||
// series.
|
|
||||||
if (config.stacked) {
|
|
||||||
needsMeasureOffset = true;
|
|
||||||
var domain = domainFn(barIndex);
|
|
||||||
var measure = measureFn(barIndex);
|
|
||||||
|
|
||||||
// We will render positive bars in one stack, and negative bars in a
|
|
||||||
// separate stack. Keep track of the measure offsets for these stacks
|
|
||||||
// independently.
|
|
||||||
var domainToCategoryToDetailsMap = measure == null || measure >= 0
|
|
||||||
? posDomainToStackKeyToDetailsMap
|
|
||||||
: negDomainToStackKeyToDetailsMap;
|
|
||||||
|
|
||||||
var categoryToDetailsMap =
|
|
||||||
domainToCategoryToDetailsMap.putIfAbsent(domain, () => {});
|
|
||||||
|
|
||||||
var prevDetail = categoryToDetailsMap[stackKey];
|
|
||||||
|
|
||||||
if (prevDetail != null) {
|
|
||||||
details.barStackIndex = prevDetail.barStackIndex + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
details.cumulativeTotal = measure != null ? measure : 0;
|
|
||||||
|
|
||||||
// Get the previous series' measure offset.
|
|
||||||
var measureOffset = measureOffsetFn(barIndex);
|
|
||||||
if (prevDetail != null) {
|
|
||||||
measureOffset += prevDetail.measureOffsetPlusMeasure;
|
|
||||||
|
|
||||||
details.cumulativeTotal += prevDetail.cumulativeTotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// And overwrite the details measure offset.
|
|
||||||
details.measureOffset = measureOffset;
|
|
||||||
var measureValue = (measure != null ? measure : 0);
|
|
||||||
details.measureOffsetPlusMeasure = measureOffset + measureValue;
|
|
||||||
|
|
||||||
categoryToDetailsMap[stackKey] = details;
|
|
||||||
}
|
|
||||||
|
|
||||||
maxBarStackSize = max(maxBarStackSize, details.barStackIndex + 1);
|
|
||||||
|
|
||||||
elements.add(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (needsMeasureOffset) {
|
|
||||||
// Override the measure offset function to return the measure offset we
|
|
||||||
// calculated for each datum. This already includes any measure offset
|
|
||||||
// that was configured in the series data.
|
|
||||||
series.measureOffsetFn = (index) => elements[index].measureOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
series.setAttr(barGroupIndexKey, barGroupIndex);
|
|
||||||
series.setAttr(stackKeyKey, stackKey);
|
|
||||||
series.setAttr(barElementsKey, elements);
|
|
||||||
|
|
||||||
if (config.grouped) {
|
|
||||||
barGroupIndex++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Compute number of bar groups. This must be done after we have processed
|
|
||||||
// all of the series once, so that we know how many categories we have.
|
|
||||||
var numBarGroups = 0;
|
|
||||||
if (config.grouped && config.stacked) {
|
|
||||||
// For grouped stacked bars, categoryToIndexMap effectively one list per
|
|
||||||
// group of stacked bars.
|
|
||||||
numBarGroups = categoryToIndexMap.length;
|
|
||||||
} else if (config.stacked) {
|
|
||||||
numBarGroups = 1;
|
|
||||||
} else {
|
|
||||||
numBarGroups = seriesList.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute bar group weights.
|
|
||||||
final barWeights = _calculateBarWeights(numBarGroups);
|
|
||||||
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
series.setAttr(barGroupCountKey, numBarGroups);
|
|
||||||
|
|
||||||
if (barWeights.isNotEmpty) {
|
|
||||||
final barGroupIndex = series.getAttr(barGroupIndexKey);
|
|
||||||
final barWeight = barWeights[barGroupIndex];
|
|
||||||
|
|
||||||
// In RTL mode, we need to grab the weights for the bars that follow
|
|
||||||
// this datum in the series (instead of precede it). The first datum is
|
|
||||||
// physically positioned on the canvas to the right of all the rest of
|
|
||||||
// the bar group data that follows it.
|
|
||||||
final previousBarWeights = isRtl
|
|
||||||
? barWeights.getRange(barGroupIndex + 1, numBarGroups)
|
|
||||||
: barWeights.getRange(0, barGroupIndex);
|
|
||||||
|
|
||||||
final previousBarWeight = previousBarWeights.isNotEmpty
|
|
||||||
? previousBarWeights.reduce((a, b) => a + b)
|
|
||||||
: 0.0;
|
|
||||||
|
|
||||||
series.setAttr(barGroupWeightKey, barWeight);
|
|
||||||
series.setAttr(previousBarGroupWeightKey, previousBarWeight);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates bar weights for a list of series from [config.weightPattern].
|
|
||||||
///
|
|
||||||
/// If [config.weightPattern] is not set, then this will assign a weight
|
|
||||||
/// proportional to the number of bar groups for every series.
|
|
||||||
List<double> _calculateBarWeights(int numBarGroups) {
|
|
||||||
// Set up bar weights for each series as a ratio of the total weight.
|
|
||||||
final weights = <double>[];
|
|
||||||
|
|
||||||
if (config.weightPattern != null) {
|
|
||||||
if (numBarGroups > config.weightPattern.length) {
|
|
||||||
throw ArgumentError('Number of series exceeds length of weight '
|
|
||||||
'pattern ${config.weightPattern}');
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalBarWeight = 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < numBarGroups; i++) {
|
|
||||||
totalBarWeight += config.weightPattern[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < numBarGroups; i++) {
|
|
||||||
weights.add(config.weightPattern[i] / totalBarWeight);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (var i = 0; i < numBarGroups; i++) {
|
|
||||||
weights.add(1 / numBarGroups);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return weights;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct a base details element for a given datum.
|
|
||||||
///
|
|
||||||
/// This is intended to be overridden by child classes that need to add
|
|
||||||
/// customized rendering properties.
|
|
||||||
R getBaseDetails(dynamic datum, int index);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void configureDomainAxes(List<MutableSeries<D>> seriesList) {
|
|
||||||
super.configureDomainAxes(seriesList);
|
|
||||||
|
|
||||||
// Configure the domain axis to use a range band configuration.
|
|
||||||
if (seriesList.isNotEmpty) {
|
|
||||||
// Given that charts can only have one domain axis, just grab it from the
|
|
||||||
// first series.
|
|
||||||
final domainAxis = seriesList.first.getAttr(domainAxisKey);
|
|
||||||
domainAxis.setRangeBandConfig(RangeBandConfig.styleAssignedPercent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void update(List<ImmutableSeries<D>> seriesList, bool isAnimatingThisDraw) {
|
|
||||||
_currentKeys.clear();
|
|
||||||
_currentGroupsStackKeys.clear();
|
|
||||||
|
|
||||||
final orderedSeriesList = getOrderedSeriesList(seriesList);
|
|
||||||
|
|
||||||
orderedSeriesList.forEach((final series) {
|
|
||||||
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
|
|
||||||
final domainFn = series.domainFn;
|
|
||||||
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
|
|
||||||
final measureFn = series.measureFn;
|
|
||||||
final colorFn = series.colorFn;
|
|
||||||
final dashPatternFn = series.dashPatternFn;
|
|
||||||
final fillColorFn = series.fillColorFn;
|
|
||||||
final seriesStackKey = series.getAttr(stackKeyKey);
|
|
||||||
final barGroupCount = series.getAttr(barGroupCountKey);
|
|
||||||
final barGroupIndex = series.getAttr(barGroupIndexKey);
|
|
||||||
final previousBarGroupWeight = series.getAttr(previousBarGroupWeightKey);
|
|
||||||
final barGroupWeight = series.getAttr(barGroupWeightKey);
|
|
||||||
final measureAxisPosition = measureAxis.getLocation(0.0);
|
|
||||||
|
|
||||||
var elementsList = series.getAttr(barElementsKey);
|
|
||||||
|
|
||||||
// Save off domainAxis for getNearest.
|
|
||||||
_prevDomainAxis = domainAxis;
|
|
||||||
|
|
||||||
for (var barIndex = 0; barIndex < series.data.length; barIndex++) {
|
|
||||||
final datum = series.data[barIndex];
|
|
||||||
BaseBarRendererElement details = elementsList[barIndex];
|
|
||||||
D domainValue = domainFn(barIndex);
|
|
||||||
|
|
||||||
final measureValue = measureFn(barIndex);
|
|
||||||
final measureIsNull = measureValue == null;
|
|
||||||
final measureIsNegative = !measureIsNull && measureValue < 0;
|
|
||||||
|
|
||||||
// Each bar should be stored in barStackMap in a structure that mirrors
|
|
||||||
// the visual rendering of the bars. Thus, they should be grouped by
|
|
||||||
// domain value, series category (by way of the stack keys that were
|
|
||||||
// generated for each series in the preprocess step), and bar group
|
|
||||||
// index to account for all combinations of grouping and stacking.
|
|
||||||
var barStackMapKey = domainValue.toString() +
|
|
||||||
'__' +
|
|
||||||
seriesStackKey +
|
|
||||||
'__' +
|
|
||||||
(measureIsNegative ? 'pos' : 'neg') +
|
|
||||||
'__' +
|
|
||||||
barGroupIndex.toString();
|
|
||||||
|
|
||||||
var barKey = barStackMapKey + details.barStackIndex.toString();
|
|
||||||
|
|
||||||
var barStackList = _barStackMap.putIfAbsent(barStackMapKey, () => []);
|
|
||||||
|
|
||||||
// If we already have an AnimatingBarfor that index, use it.
|
|
||||||
var animatingBar = barStackList.firstWhere((bar) => bar.key == barKey,
|
|
||||||
orElse: () => null);
|
|
||||||
|
|
||||||
// If we don't have any existing bar element, create a new bar and have
|
|
||||||
// it animate in from the domain axis.
|
|
||||||
// TODO: Animate bars in the middle of a stack from their
|
|
||||||
// nearest neighbors, instead of the measure axis.
|
|
||||||
if (animatingBar == null) {
|
|
||||||
// If the measure is null and there was no existing animating bar, it
|
|
||||||
// means we don't need to draw this bar at all.
|
|
||||||
if (!measureIsNull) {
|
|
||||||
animatingBar = makeAnimatedBar(
|
|
||||||
key: barKey,
|
|
||||||
series: series,
|
|
||||||
datum: datum,
|
|
||||||
barGroupIndex: barGroupIndex,
|
|
||||||
previousBarGroupWeight: previousBarGroupWeight,
|
|
||||||
barGroupWeight: barGroupWeight,
|
|
||||||
color: colorFn(barIndex),
|
|
||||||
dashPattern: dashPatternFn(barIndex),
|
|
||||||
details: details,
|
|
||||||
domainValue: domainFn(barIndex),
|
|
||||||
domainAxis: domainAxis,
|
|
||||||
domainWidth: domainAxis.rangeBand.round(),
|
|
||||||
fillColor: fillColorFn(barIndex),
|
|
||||||
fillPattern: details.fillPattern,
|
|
||||||
measureValue: 0.0,
|
|
||||||
measureOffsetValue: 0.0,
|
|
||||||
measureAxisPosition: measureAxisPosition,
|
|
||||||
measureAxis: measureAxis,
|
|
||||||
numBarGroups: barGroupCount,
|
|
||||||
strokeWidthPx: details.strokeWidthPx,
|
|
||||||
measureIsNull: measureIsNull,
|
|
||||||
measureIsNegative: measureIsNegative);
|
|
||||||
|
|
||||||
barStackList.add(animatingBar);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
animatingBar
|
|
||||||
..datum = datum
|
|
||||||
..series = series
|
|
||||||
..domainValue = domainValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (animatingBar == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the set of bars that still exist in the series data.
|
|
||||||
_currentKeys.add(barKey);
|
|
||||||
|
|
||||||
// Store off stack keys for each bar group to help getNearest identify
|
|
||||||
// groups of stacks.
|
|
||||||
_currentGroupsStackKeys
|
|
||||||
.putIfAbsent(domainValue, () => <String>{})
|
|
||||||
.add(barStackMapKey);
|
|
||||||
|
|
||||||
// Get the barElement we are going to setup.
|
|
||||||
// Optimization to prevent allocation in non-animating case.
|
|
||||||
BaseBarRendererElement barElement = makeBarRendererElement(
|
|
||||||
barGroupIndex: barGroupIndex,
|
|
||||||
previousBarGroupWeight: previousBarGroupWeight,
|
|
||||||
barGroupWeight: barGroupWeight,
|
|
||||||
color: colorFn(barIndex),
|
|
||||||
dashPattern: dashPatternFn(barIndex),
|
|
||||||
details: details,
|
|
||||||
domainValue: domainFn(barIndex),
|
|
||||||
domainAxis: domainAxis,
|
|
||||||
domainWidth: domainAxis.rangeBand.round(),
|
|
||||||
fillColor: fillColorFn(barIndex),
|
|
||||||
fillPattern: details.fillPattern,
|
|
||||||
measureValue: measureValue,
|
|
||||||
measureOffsetValue: details.measureOffset,
|
|
||||||
measureAxisPosition: measureAxisPosition,
|
|
||||||
measureAxis: measureAxis,
|
|
||||||
numBarGroups: barGroupCount,
|
|
||||||
strokeWidthPx: details.strokeWidthPx,
|
|
||||||
measureIsNull: measureIsNull,
|
|
||||||
measureIsNegative: measureIsNegative);
|
|
||||||
|
|
||||||
animatingBar.setNewTarget(barElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate out bars that don't exist anymore.
|
|
||||||
_barStackMap.forEach((key, barStackList) {
|
|
||||||
for (var barIndex = 0; barIndex < barStackList.length; barIndex++) {
|
|
||||||
final bar = barStackList[barIndex];
|
|
||||||
if (_currentKeys.contains(bar.key) != true) {
|
|
||||||
bar.animateOut();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a [BaseAnimatedBar] to represent the previous and current state
|
|
||||||
/// of one bar on the chart.
|
|
||||||
B makeAnimatedBar(
|
|
||||||
{String key,
|
|
||||||
ImmutableSeries<D> series,
|
|
||||||
dynamic datum,
|
|
||||||
int barGroupIndex,
|
|
||||||
double previousBarGroupWeight,
|
|
||||||
double barGroupWeight,
|
|
||||||
Color color,
|
|
||||||
List<int> dashPattern,
|
|
||||||
R details,
|
|
||||||
D domainValue,
|
|
||||||
ImmutableAxis<D> domainAxis,
|
|
||||||
int domainWidth,
|
|
||||||
num measureValue,
|
|
||||||
num measureOffsetValue,
|
|
||||||
ImmutableAxis<num> measureAxis,
|
|
||||||
double measureAxisPosition,
|
|
||||||
int numBarGroups,
|
|
||||||
Color fillColor,
|
|
||||||
FillPatternType fillPattern,
|
|
||||||
double strokeWidthPx,
|
|
||||||
bool measureIsNull,
|
|
||||||
bool measureIsNegative});
|
|
||||||
|
|
||||||
/// Generates a [BaseBarRendererElement] to represent the rendering data for
|
|
||||||
/// one bar on the chart.
|
|
||||||
R makeBarRendererElement(
|
|
||||||
{int barGroupIndex,
|
|
||||||
double previousBarGroupWeight,
|
|
||||||
double barGroupWeight,
|
|
||||||
Color color,
|
|
||||||
List<int> dashPattern,
|
|
||||||
R details,
|
|
||||||
D domainValue,
|
|
||||||
ImmutableAxis<D> domainAxis,
|
|
||||||
int domainWidth,
|
|
||||||
num measureValue,
|
|
||||||
num measureOffsetValue,
|
|
||||||
ImmutableAxis<num> measureAxis,
|
|
||||||
double measureAxisPosition,
|
|
||||||
int numBarGroups,
|
|
||||||
Color fillColor,
|
|
||||||
FillPatternType fillPattern,
|
|
||||||
double strokeWidthPx,
|
|
||||||
bool measureIsNull,
|
|
||||||
bool measureIsNegative});
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onAttach(BaseChart<D> chart) {
|
|
||||||
super.onAttach(chart);
|
|
||||||
// We only need the chart.context.isRtl setting, but context is not yet
|
|
||||||
// available when the default renderer is attached to the chart on chart
|
|
||||||
// creation time, since chart onInit is called after the chart is created.
|
|
||||||
this.chart = chart;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Paints the current bar data on the canvas.
|
|
||||||
void paint(ChartCanvas canvas, double animationPercent) {
|
|
||||||
// Clean up the bars that no longer exist.
|
|
||||||
if (animationPercent == 1.0) {
|
|
||||||
final keysToRemove = HashSet<String>();
|
|
||||||
|
|
||||||
_barStackMap.forEach((key, barStackList) {
|
|
||||||
barStackList.retainWhere(
|
|
||||||
(bar) => !bar.animatingOut && !bar.targetBar.measureIsNull);
|
|
||||||
|
|
||||||
if (barStackList.isEmpty) {
|
|
||||||
keysToRemove.add(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// When cleaning up the animation, also clean up the keys used to lookup
|
|
||||||
// if a bar is selected.
|
|
||||||
for (String key in keysToRemove) {
|
|
||||||
_barStackMap.remove(key);
|
|
||||||
_currentKeys.remove(key);
|
|
||||||
}
|
|
||||||
_currentGroupsStackKeys.forEach((domain, keys) {
|
|
||||||
keys.removeWhere(keysToRemove.contains);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_barStackMap.forEach((stackKey, barStack) {
|
|
||||||
// Turn this into a list so that the getCurrentBar isn't called more than
|
|
||||||
// once for each animationPercent if the barElements are iterated more
|
|
||||||
// than once.
|
|
||||||
final barElements = barStack
|
|
||||||
.map((animatingBar) => animatingBar.getCurrentBar(animationPercent))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (barElements.isNotEmpty) {
|
|
||||||
paintBar(canvas, animationPercent, barElements);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Paints a stack of bar elements on the canvas.
|
|
||||||
void paintBar(
|
|
||||||
ChartCanvas canvas, double animationPercent, Iterable<R> barElements);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<DatumDetails<D>> getNearestDatumDetailPerSeries(
|
|
||||||
Point<double> chartPoint, bool byDomain, Rectangle<int> boundsOverride) {
|
|
||||||
var nearest = <DatumDetails<D>>[];
|
|
||||||
|
|
||||||
// Was it even in the component bounds?
|
|
||||||
if (!isPointWithinBounds(chartPoint, boundsOverride)) {
|
|
||||||
return nearest;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_prevDomainAxis is OrdinalAxis) {
|
|
||||||
final domainValue = _prevDomainAxis
|
|
||||||
.getDomain(renderingVertically ? chartPoint.x : chartPoint.y);
|
|
||||||
|
|
||||||
// If we have a domainValue for the event point, then find all segments
|
|
||||||
// that match it.
|
|
||||||
if (domainValue != null) {
|
|
||||||
if (renderingVertically) {
|
|
||||||
nearest = _getVerticalDetailsForDomainValue(domainValue, chartPoint);
|
|
||||||
} else {
|
|
||||||
nearest =
|
|
||||||
_getHorizontalDetailsForDomainValue(domainValue, chartPoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (renderingVertically) {
|
|
||||||
nearest = _getVerticalDetailsForDomainValue(null, chartPoint);
|
|
||||||
} else {
|
|
||||||
nearest = _getHorizontalDetailsForDomainValue(null, chartPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the closest domain and only keep values that match the domain.
|
|
||||||
var minRelativeDistance = double.maxFinite;
|
|
||||||
var minDomainDistance = double.maxFinite;
|
|
||||||
var minMeasureDistance = double.maxFinite;
|
|
||||||
D nearestDomain;
|
|
||||||
|
|
||||||
// TODO: Optimize this with a binary search based on chartX.
|
|
||||||
for (DatumDetails<D> detail in nearest) {
|
|
||||||
if (byDomain) {
|
|
||||||
if (detail.domainDistance < minDomainDistance ||
|
|
||||||
(detail.domainDistance == minDomainDistance &&
|
|
||||||
detail.measureDistance < minMeasureDistance)) {
|
|
||||||
minDomainDistance = detail.domainDistance;
|
|
||||||
minMeasureDistance = detail.measureDistance;
|
|
||||||
nearestDomain = detail.domain;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (detail.relativeDistance < minRelativeDistance) {
|
|
||||||
minRelativeDistance = detail.relativeDistance;
|
|
||||||
nearestDomain = detail.domain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nearest.retainWhere((d) => d.domain == nearestDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't find anything, then keep an empty list.
|
|
||||||
nearest ??= <DatumDetails<D>>[];
|
|
||||||
|
|
||||||
// Note: the details are already sorted by domain & measure distance in
|
|
||||||
// base chart.
|
|
||||||
return nearest;
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle<int> getBoundsForBar(R bar);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
List<BaseAnimatedBar<D, R>> _getSegmentsForDomainValue(D domainValue,
|
|
||||||
{bool where(BaseAnimatedBar<D, R> bar)}) {
|
|
||||||
final matchingSegments = <BaseAnimatedBar<D, R>>[];
|
|
||||||
|
|
||||||
// [domainValue] is null only when the bar renderer is being used with in
|
|
||||||
// a non ordinal axis (ex. date time axis).
|
|
||||||
//
|
|
||||||
// In the case of null [domainValue] return all values to be compared, since
|
|
||||||
// we can't use the optimized comparison for [OrdinalAxis].
|
|
||||||
final stackKeys = (domainValue != null)
|
|
||||||
? _currentGroupsStackKeys[domainValue]
|
|
||||||
: _currentGroupsStackKeys.values
|
|
||||||
.reduce((allKeys, keys) => allKeys..addAll(keys));
|
|
||||||
stackKeys?.forEach((stackKey) {
|
|
||||||
if (where != null) {
|
|
||||||
matchingSegments.addAll(_barStackMap[stackKey].where(where));
|
|
||||||
} else {
|
|
||||||
matchingSegments.addAll(_barStackMap[stackKey]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return matchingSegments;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the case of null [domainValue] return all values to be compared, since
|
|
||||||
// we can't use the optimized comparison for [OrdinalAxis].
|
|
||||||
List<DatumDetails<D>> _getVerticalDetailsForDomainValue(
|
|
||||||
D domainValue, Point<double> chartPoint) {
|
|
||||||
return List<DatumDetails<D>>.from(_getSegmentsForDomainValue(domainValue,
|
|
||||||
where: (bar) => !bar.series.overlaySeries).map<DatumDetails<D>>((bar) {
|
|
||||||
final barBounds = getBoundsForBar(bar.currentBar);
|
|
||||||
final segmentDomainDistance =
|
|
||||||
_getDistance(chartPoint.x.round(), barBounds.left, barBounds.right);
|
|
||||||
final segmentMeasureDistance =
|
|
||||||
_getDistance(chartPoint.y.round(), barBounds.top, barBounds.bottom);
|
|
||||||
|
|
||||||
final nearestPoint = Point<double>(
|
|
||||||
clamp(chartPoint.x, barBounds.left, barBounds.right).toDouble(),
|
|
||||||
clamp(chartPoint.y, barBounds.top, barBounds.bottom).toDouble());
|
|
||||||
|
|
||||||
final relativeDistance = chartPoint.distanceTo(nearestPoint);
|
|
||||||
|
|
||||||
return DatumDetails<D>(
|
|
||||||
series: bar.series,
|
|
||||||
datum: bar.datum,
|
|
||||||
domain: bar.domainValue,
|
|
||||||
domainDistance: segmentDomainDistance,
|
|
||||||
measureDistance: segmentMeasureDistance,
|
|
||||||
relativeDistance: relativeDistance,
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
List<DatumDetails<D>> _getHorizontalDetailsForDomainValue(
|
|
||||||
D domainValue, Point<double> chartPoint) {
|
|
||||||
return List<DatumDetails<D>>.from(_getSegmentsForDomainValue(domainValue,
|
|
||||||
where: (bar) => !bar.series.overlaySeries).map((bar) {
|
|
||||||
final barBounds = getBoundsForBar(bar.currentBar);
|
|
||||||
final segmentDomainDistance =
|
|
||||||
_getDistance(chartPoint.y.round(), barBounds.top, barBounds.bottom);
|
|
||||||
final segmentMeasureDistance =
|
|
||||||
_getDistance(chartPoint.x.round(), barBounds.left, barBounds.right);
|
|
||||||
|
|
||||||
return DatumDetails<D>(
|
|
||||||
series: bar.series,
|
|
||||||
datum: bar.datum,
|
|
||||||
domain: bar.domainValue,
|
|
||||||
domainDistance: segmentDomainDistance,
|
|
||||||
measureDistance: segmentMeasureDistance,
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
double _getDistance(int point, int min, int max) {
|
|
||||||
if (max >= point && min <= point) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
return (point > max ? (point - max) : (min - point)).toDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the iterator for the series based grouped/stacked and orientation.
|
|
||||||
///
|
|
||||||
/// For vertical stacked bars:
|
|
||||||
/// * If grouped, return the iterator that keeps the category order but
|
|
||||||
/// reverse the order of the series so the first series is on the top of the
|
|
||||||
/// stack.
|
|
||||||
/// * Otherwise, return iterator of the reversed list
|
|
||||||
///
|
|
||||||
/// All other types, use the in order iterator.
|
|
||||||
@protected
|
|
||||||
Iterable<S> getOrderedSeriesList<S extends ImmutableSeries>(
|
|
||||||
List<S> seriesList) {
|
|
||||||
return (renderingVertically && config.stacked)
|
|
||||||
? config.grouped
|
|
||||||
? _ReversedSeriesIterable(seriesList)
|
|
||||||
: seriesList.reversed
|
|
||||||
: seriesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isRtl => chart.context.isRtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterable wrapping the seriesList that returns the ReversedSeriesItertor.
|
|
||||||
class _ReversedSeriesIterable<S extends ImmutableSeries> extends Iterable<S> {
|
|
||||||
final List<S> seriesList;
|
|
||||||
|
|
||||||
_ReversedSeriesIterable(this.seriesList);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Iterator<S> get iterator => _ReversedSeriesIterator(seriesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterator that keeps reverse series order but keeps category order.
|
|
||||||
///
|
|
||||||
/// This is needed because for grouped stacked bars, the category stays in the
|
|
||||||
/// order it was passed in for the grouping, but the series is flipped so that
|
|
||||||
/// the first series of that category is on the top of the stack.
|
|
||||||
class _ReversedSeriesIterator<S extends ImmutableSeries> extends Iterator<S> {
|
|
||||||
final List<S> _list;
|
|
||||||
final _visitIndex = <int>[];
|
|
||||||
int _current;
|
|
||||||
|
|
||||||
_ReversedSeriesIterator(List<S> list) : _list = list {
|
|
||||||
// In the order of the list, save the category and the indices of the series
|
|
||||||
// with the same category.
|
|
||||||
final categoryAndSeriesIndexMap = <String, List<int>>{};
|
|
||||||
for (var i = 0; i < list.length; i++) {
|
|
||||||
categoryAndSeriesIndexMap
|
|
||||||
.putIfAbsent(list[i].seriesCategory, () => <int>[])
|
|
||||||
.add(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a visit that is categories in order, but the series is reversed.
|
|
||||||
categoryAndSeriesIndexMap
|
|
||||||
.forEach((_, indices) => _visitIndex.addAll(indices.reversed));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool moveNext() {
|
|
||||||
_current = (_current == null) ? 0 : _current + 1;
|
|
||||||
|
|
||||||
return _current < _list.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
S get current => _list[_visitIndex[_current]];
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart' show ListEquality;
|
|
||||||
|
|
||||||
import '../../common/symbol_renderer.dart'
|
|
||||||
show SymbolRenderer, RoundedRectSymbolRenderer;
|
|
||||||
import '../common/chart_canvas.dart' show FillPatternType;
|
|
||||||
import '../common/series_renderer_config.dart'
|
|
||||||
show RendererAttributes, SeriesRendererConfig;
|
|
||||||
import '../layout/layout_view.dart' show LayoutViewConfig;
|
|
||||||
|
|
||||||
/// Shared configuration for bar chart renderers.
|
|
||||||
///
|
|
||||||
/// Bar renderers support 4 different modes of rendering multiple series on the
|
|
||||||
/// chart, configured by the grouped and stacked flags.
|
|
||||||
/// * grouped - Render bars for each series that shares a domain value
|
|
||||||
/// side-by-side.
|
|
||||||
/// * stacked - Render bars for each series that shares a domain value in a
|
|
||||||
/// stack, ordered in the same order as the series list.
|
|
||||||
/// * grouped-stacked: Render bars for each series that shares a domain value in
|
|
||||||
/// a group of bar stacks. Each stack will contain all the series that share a
|
|
||||||
/// series category.
|
|
||||||
/// * floating style - When grouped and stacked are both false, all bars that
|
|
||||||
/// share a domain value will be rendered in the same domain space. Each datum
|
|
||||||
/// should be configured with a measure offset to position its bar along the
|
|
||||||
/// measure axis. Bars will freely overlap if their measure values and measure
|
|
||||||
/// offsets overlap. Note that bars for each series will be rendered in order,
|
|
||||||
/// such that bars from the last series will be "on top" of bars from previous
|
|
||||||
/// series.
|
|
||||||
abstract class BaseBarRendererConfig<D> extends LayoutViewConfig
|
|
||||||
implements SeriesRendererConfig<D> {
|
|
||||||
final String customRendererId;
|
|
||||||
|
|
||||||
final SymbolRenderer symbolRenderer;
|
|
||||||
|
|
||||||
/// Dash pattern for the stroke line around the edges of the bar.
|
|
||||||
final List<int> dashPattern;
|
|
||||||
|
|
||||||
/// Defines the way multiple series of bars are rendered per domain.
|
|
||||||
final BarGroupingType groupingType;
|
|
||||||
|
|
||||||
/// The order to paint this renderer on the canvas.
|
|
||||||
final int layoutPaintOrder;
|
|
||||||
|
|
||||||
final int minBarLengthPx;
|
|
||||||
|
|
||||||
final FillPatternType fillPattern;
|
|
||||||
|
|
||||||
final double stackHorizontalSeparator;
|
|
||||||
|
|
||||||
/// Stroke width of the target line.
|
|
||||||
final double strokeWidthPx;
|
|
||||||
|
|
||||||
/// Sets the series weight pattern. This is a pattern of weights used to
|
|
||||||
/// calculate the width of bars within a bar group. If not specified, each bar
|
|
||||||
/// in the group will have an equal width.
|
|
||||||
///
|
|
||||||
/// The pattern will not repeat. If more series are assigned to the renderer
|
|
||||||
/// than there are segments in the weight pattern, an error will be thrown.
|
|
||||||
///
|
|
||||||
/// e.g. For the pattern [2, 1], the first bar in a group should be rendered
|
|
||||||
/// twice as wide as the second bar.
|
|
||||||
///
|
|
||||||
/// If the expected bar width of the chart is 12px, then the first bar will
|
|
||||||
/// render at 16px and the second will render at 8px. The default weight
|
|
||||||
/// pattern of null means that all bars should be the same width, or 12px in
|
|
||||||
/// this case.
|
|
||||||
///
|
|
||||||
/// Not used for stacked bars.
|
|
||||||
final List<int> weightPattern;
|
|
||||||
|
|
||||||
final rendererAttributes = RendererAttributes();
|
|
||||||
|
|
||||||
BaseBarRendererConfig(
|
|
||||||
{this.customRendererId,
|
|
||||||
this.dashPattern,
|
|
||||||
this.groupingType = BarGroupingType.grouped,
|
|
||||||
this.layoutPaintOrder,
|
|
||||||
this.minBarLengthPx = 0,
|
|
||||||
this.fillPattern,
|
|
||||||
this.stackHorizontalSeparator,
|
|
||||||
this.strokeWidthPx = 0.0,
|
|
||||||
SymbolRenderer symbolRenderer,
|
|
||||||
this.weightPattern})
|
|
||||||
: this.symbolRenderer = symbolRenderer ?? RoundedRectSymbolRenderer();
|
|
||||||
|
|
||||||
/// Whether or not the bars should be organized into groups.
|
|
||||||
bool get grouped =>
|
|
||||||
groupingType == BarGroupingType.grouped ||
|
|
||||||
groupingType == BarGroupingType.groupedStacked;
|
|
||||||
|
|
||||||
/// Whether or not the bars should be organized into stacks.
|
|
||||||
bool get stacked =>
|
|
||||||
groupingType == BarGroupingType.stacked ||
|
|
||||||
groupingType == BarGroupingType.groupedStacked;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) {
|
|
||||||
if (identical(this, other)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return other is BaseBarRendererConfig &&
|
|
||||||
other.customRendererId == customRendererId &&
|
|
||||||
other.dashPattern == dashPattern &&
|
|
||||||
other.fillPattern == fillPattern &&
|
|
||||||
other.groupingType == groupingType &&
|
|
||||||
other.minBarLengthPx == minBarLengthPx &&
|
|
||||||
other.stackHorizontalSeparator == stackHorizontalSeparator &&
|
|
||||||
other.strokeWidthPx == strokeWidthPx &&
|
|
||||||
other.symbolRenderer == symbolRenderer &&
|
|
||||||
ListEquality().equals(other.weightPattern, weightPattern);
|
|
||||||
}
|
|
||||||
|
|
||||||
int get hashcode {
|
|
||||||
var hash = 1;
|
|
||||||
hash = hash * 31 + (customRendererId?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (dashPattern?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (fillPattern?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (groupingType?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (minBarLengthPx?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (stackHorizontalSeparator?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (strokeWidthPx?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (symbolRenderer?.hashCode ?? 0);
|
|
||||||
hash = hash * 31 + (weightPattern?.hashCode ?? 0);
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Defines the way multiple series of bars are renderered per domain.
|
|
||||||
///
|
|
||||||
/// * [grouped] - Render bars for each series that shares a domain value
|
|
||||||
/// side-by-side.
|
|
||||||
/// * [stacked] - Render bars for each series that shares a domain value in a
|
|
||||||
/// stack, ordered in the same order as the series list.
|
|
||||||
/// * [groupedStacked]: Render bars for each series that shares a domain value
|
|
||||||
/// in a group of bar stacks. Each stack will contain all the series that
|
|
||||||
/// share a series category.
|
|
||||||
enum BarGroupingType { grouped, groupedStacked, stacked }
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../common/color.dart' show Color;
|
|
||||||
import '../common/chart_canvas.dart' show getAnimatedColor, FillPatternType;
|
|
||||||
import '../common/processed_series.dart' show ImmutableSeries;
|
|
||||||
|
|
||||||
abstract class BaseBarRendererElement {
|
|
||||||
int barStackIndex;
|
|
||||||
Color color;
|
|
||||||
num cumulativeTotal;
|
|
||||||
List<int> dashPattern;
|
|
||||||
Color fillColor;
|
|
||||||
FillPatternType fillPattern;
|
|
||||||
double measureAxisPosition;
|
|
||||||
num measureOffset;
|
|
||||||
num measureOffsetPlusMeasure;
|
|
||||||
double strokeWidthPx;
|
|
||||||
bool measureIsNull;
|
|
||||||
bool measureIsNegative;
|
|
||||||
|
|
||||||
BaseBarRendererElement();
|
|
||||||
|
|
||||||
BaseBarRendererElement.clone(BaseBarRendererElement other) {
|
|
||||||
barStackIndex = other.barStackIndex;
|
|
||||||
color = other.color != null ? Color.fromOther(color: other.color) : null;
|
|
||||||
cumulativeTotal = other.cumulativeTotal;
|
|
||||||
dashPattern = other.dashPattern;
|
|
||||||
fillColor = other.fillColor != null
|
|
||||||
? Color.fromOther(color: other.fillColor)
|
|
||||||
: null;
|
|
||||||
fillPattern = other.fillPattern;
|
|
||||||
measureAxisPosition = other.measureAxisPosition;
|
|
||||||
measureOffset = other.measureOffset;
|
|
||||||
measureOffsetPlusMeasure = other.measureOffsetPlusMeasure;
|
|
||||||
strokeWidthPx = other.strokeWidthPx;
|
|
||||||
measureIsNull = other.measureIsNull;
|
|
||||||
measureIsNegative = other.measureIsNegative;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateAnimationPercent(BaseBarRendererElement previous,
|
|
||||||
BaseBarRendererElement target, double animationPercent) {
|
|
||||||
color = getAnimatedColor(previous.color, target.color, animationPercent);
|
|
||||||
fillColor = getAnimatedColor(
|
|
||||||
previous.fillColor, target.fillColor, animationPercent);
|
|
||||||
measureIsNull = target.measureIsNull;
|
|
||||||
measureIsNegative = target.measureIsNegative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class BaseAnimatedBar<D, R extends BaseBarRendererElement> {
|
|
||||||
final String key;
|
|
||||||
dynamic datum;
|
|
||||||
ImmutableSeries<D> series;
|
|
||||||
D domainValue;
|
|
||||||
|
|
||||||
R _previousBar;
|
|
||||||
R _targetBar;
|
|
||||||
R _currentBar;
|
|
||||||
|
|
||||||
// Flag indicating whether this bar is being animated out of the chart.
|
|
||||||
bool animatingOut = false;
|
|
||||||
|
|
||||||
BaseAnimatedBar({this.key, this.datum, this.series, this.domainValue});
|
|
||||||
|
|
||||||
/// Animates a bar that was removed from the series out of the view.
|
|
||||||
///
|
|
||||||
/// This should be called in place of "setNewTarget" for bars that represent
|
|
||||||
/// data that has been removed from the series.
|
|
||||||
///
|
|
||||||
/// Animates the height of the bar down to the measure axis position (position
|
|
||||||
/// of 0). Animates the width of the bar down to 0, centered in the middle of
|
|
||||||
/// the original bar width.
|
|
||||||
void animateOut() {
|
|
||||||
var newTarget = clone(_currentBar);
|
|
||||||
|
|
||||||
animateElementToMeasureAxisPosition(newTarget);
|
|
||||||
|
|
||||||
setNewTarget(newTarget);
|
|
||||||
animatingOut = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the bounds for the target to the measure axis position.
|
|
||||||
void animateElementToMeasureAxisPosition(R target);
|
|
||||||
|
|
||||||
/// Sets a new element to render.
|
|
||||||
void setNewTarget(R newTarget) {
|
|
||||||
animatingOut = false;
|
|
||||||
_currentBar ??= clone(newTarget);
|
|
||||||
_previousBar = clone(_currentBar);
|
|
||||||
_targetBar = newTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
R get currentBar => _currentBar;
|
|
||||||
|
|
||||||
R get previousBar => _previousBar;
|
|
||||||
|
|
||||||
R get targetBar => _targetBar;
|
|
||||||
|
|
||||||
/// Gets the new state of the bar element for painting, updated for a
|
|
||||||
/// transition between the previous state and the new animationPercent.
|
|
||||||
R getCurrentBar(double animationPercent) {
|
|
||||||
if (animationPercent == 1.0 || _previousBar == null) {
|
|
||||||
_currentBar = _targetBar;
|
|
||||||
_previousBar = _targetBar;
|
|
||||||
return _currentBar;
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentBar.updateAnimationPercent(
|
|
||||||
_previousBar, _targetBar, animationPercent);
|
|
||||||
|
|
||||||
return _currentBar;
|
|
||||||
}
|
|
||||||
|
|
||||||
R clone(R bar);
|
|
||||||
}
|
|
||||||
@@ -1,572 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show Rectangle, min, max;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show protected, visibleForTesting;
|
|
||||||
|
|
||||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/text_element.dart' show TextElement;
|
|
||||||
import '../../../data/series.dart' show AttributeKey;
|
|
||||||
import '../../common/chart_canvas.dart' show ChartCanvas;
|
|
||||||
import '../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../../layout/layout_view.dart'
|
|
||||||
show
|
|
||||||
LayoutPosition,
|
|
||||||
LayoutView,
|
|
||||||
LayoutViewConfig,
|
|
||||||
LayoutViewPaintOrder,
|
|
||||||
LayoutViewPositionOrder,
|
|
||||||
ViewMeasuredSizes;
|
|
||||||
import 'axis_tick.dart' show AxisTicks;
|
|
||||||
import 'draw_strategy/small_tick_draw_strategy.dart' show SmallTickDrawStrategy;
|
|
||||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import 'linear/linear_scale.dart' show LinearScale;
|
|
||||||
import 'numeric_extents.dart' show NumericExtents;
|
|
||||||
import 'numeric_scale.dart' show NumericScale;
|
|
||||||
import 'numeric_tick_provider.dart' show NumericTickProvider;
|
|
||||||
import 'ordinal_tick_provider.dart' show OrdinalTickProvider;
|
|
||||||
import 'scale.dart'
|
|
||||||
show MutableScale, RangeBandConfig, ScaleOutputExtent, Scale;
|
|
||||||
import 'simple_ordinal_scale.dart' show SimpleOrdinalScale;
|
|
||||||
import 'tick.dart' show Tick;
|
|
||||||
import 'tick_formatter.dart'
|
|
||||||
show TickFormatter, OrdinalTickFormatter, NumericTickFormatter;
|
|
||||||
import 'tick_provider.dart' show TickProvider;
|
|
||||||
|
|
||||||
const measureAxisIdKey = AttributeKey<String>('Axis.measureAxisId');
|
|
||||||
const measureAxisKey = AttributeKey<Axis>('Axis.measureAxis');
|
|
||||||
const domainAxisKey = AttributeKey<Axis>('Axis.domainAxis');
|
|
||||||
|
|
||||||
/// Orientation of an Axis.
|
|
||||||
enum AxisOrientation { top, right, bottom, left }
|
|
||||||
|
|
||||||
abstract class ImmutableAxis<D> {
|
|
||||||
/// Compare domain to the viewport.
|
|
||||||
///
|
|
||||||
/// 0 if the domain is in the viewport.
|
|
||||||
/// 1 if the domain is to the right of the viewport.
|
|
||||||
/// -1 if the domain is to the left of the viewport.
|
|
||||||
int compareDomainValueToViewport(D domain);
|
|
||||||
|
|
||||||
/// Get location for the domain.
|
|
||||||
double getLocation(D domain);
|
|
||||||
|
|
||||||
D getDomain(double location);
|
|
||||||
|
|
||||||
/// Rangeband for this axis.
|
|
||||||
double get rangeBand;
|
|
||||||
|
|
||||||
/// Step size for this axis.
|
|
||||||
double get stepSize;
|
|
||||||
|
|
||||||
/// Output range for this axis.
|
|
||||||
ScaleOutputExtent get range;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class Axis<D> extends ImmutableAxis<D> implements LayoutView {
|
|
||||||
static const primaryMeasureAxisId = 'primaryMeasureAxisId';
|
|
||||||
static const secondaryMeasureAxisId = 'secondaryMeasureAxisId';
|
|
||||||
|
|
||||||
final MutableScale<D> _scale;
|
|
||||||
|
|
||||||
/// [Scale] of this axis.
|
|
||||||
@protected
|
|
||||||
MutableScale<D> get scale => _scale;
|
|
||||||
|
|
||||||
/// Previous [Scale] of this axis, used to calculate tick animation.
|
|
||||||
MutableScale<D> _previousScale;
|
|
||||||
|
|
||||||
/// [TickProvider] for this axis.
|
|
||||||
TickProvider<D> tickProvider;
|
|
||||||
|
|
||||||
/// [TickFormatter] for this axis.
|
|
||||||
TickFormatter<D> _tickFormatter;
|
|
||||||
|
|
||||||
set tickFormatter(TickFormatter<D> formatter) {
|
|
||||||
if (_tickFormatter != formatter) {
|
|
||||||
_tickFormatter = formatter;
|
|
||||||
_formatterValueCache.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TickFormatter<D> get tickFormatter => _tickFormatter;
|
|
||||||
final _formatterValueCache = <D, String>{};
|
|
||||||
|
|
||||||
/// [TickDrawStrategy] for this axis.
|
|
||||||
TickDrawStrategy<D> tickDrawStrategy;
|
|
||||||
|
|
||||||
/// [AxisOrientation] for this axis.
|
|
||||||
AxisOrientation axisOrientation;
|
|
||||||
|
|
||||||
ChartContext context;
|
|
||||||
|
|
||||||
/// If the output range should be reversed.
|
|
||||||
bool reverseOutputRange = false;
|
|
||||||
|
|
||||||
/// Configures whether the viewport should be reset back to default values
|
|
||||||
/// when the domain is reset.
|
|
||||||
///
|
|
||||||
/// This should generally be disabled when the viewport will be managed
|
|
||||||
/// externally, e.g. from pan and zoom behaviors.
|
|
||||||
bool autoViewport = true;
|
|
||||||
|
|
||||||
/// If the axis line should always be drawn.
|
|
||||||
bool forceDrawAxisLine;
|
|
||||||
|
|
||||||
/// If true, do not allow axis to be modified.
|
|
||||||
///
|
|
||||||
/// Ticks (including their location) are not updated.
|
|
||||||
/// Viewport changes not allowed.
|
|
||||||
bool lockAxis = false;
|
|
||||||
|
|
||||||
/// Ticks provided by the tick provider.
|
|
||||||
List<Tick> _providedTicks;
|
|
||||||
|
|
||||||
/// Ticks used by the axis for drawing.
|
|
||||||
final _axisTicks = <AxisTicks<D>>[];
|
|
||||||
|
|
||||||
Rectangle<int> _componentBounds;
|
|
||||||
Rectangle<int> _drawAreaBounds;
|
|
||||||
GraphicsFactory graphicsFactory;
|
|
||||||
|
|
||||||
/// Order for chart layout painting.
|
|
||||||
///
|
|
||||||
/// In general, domain axes should be drawn on top of measure axes to ensure
|
|
||||||
/// that the domain axis line appears on top of any measure axis grid lines.
|
|
||||||
int layoutPaintOrder = LayoutViewPaintOrder.measureAxis;
|
|
||||||
|
|
||||||
Axis(
|
|
||||||
{TickProvider<D> tickProvider,
|
|
||||||
TickFormatter<D> tickFormatter,
|
|
||||||
MutableScale<D> scale})
|
|
||||||
: this._scale = scale,
|
|
||||||
this.tickProvider = tickProvider,
|
|
||||||
this._tickFormatter = tickFormatter;
|
|
||||||
|
|
||||||
@protected
|
|
||||||
MutableScale<D> get mutableScale => _scale;
|
|
||||||
|
|
||||||
/// Rangeband for this axis.
|
|
||||||
@override
|
|
||||||
double get rangeBand => _scale.rangeBand;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get stepSize => _scale.stepSize;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ScaleOutputExtent get range => _scale.range;
|
|
||||||
|
|
||||||
void setRangeBandConfig(RangeBandConfig rangeBandConfig) {
|
|
||||||
mutableScale.rangeBandConfig = rangeBandConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
void addDomainValue(D domain) {
|
|
||||||
if (lockAxis) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_scale.addDomain(domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
void resetDomains() {
|
|
||||||
if (lockAxis) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the series list changes, clear the cache.
|
|
||||||
//
|
|
||||||
// There are cases where tick formatter has not "changed", but if measure
|
|
||||||
// formatter provided to the tick formatter uses a closure value, the
|
|
||||||
// formatter cache needs to be cleared.
|
|
||||||
//
|
|
||||||
// This type of use case for the measure formatter surfaced where the series
|
|
||||||
// list also changes. So this is a round about way to also clear the
|
|
||||||
// tick formatter cache.
|
|
||||||
//
|
|
||||||
// TODO: Measure formatter should be changed from a typedef to
|
|
||||||
// a concrete class to force users to create a new tick formatter when
|
|
||||||
// formatting is different, so we can recognize when the tick formatter is
|
|
||||||
// changed and then clear cache accordingly.
|
|
||||||
//
|
|
||||||
// Remove this when bug above is fixed, and verify it did not cause
|
|
||||||
// regression for b/110371453.
|
|
||||||
_formatterValueCache.clear();
|
|
||||||
|
|
||||||
_scale.resetDomain();
|
|
||||||
reverseOutputRange = false;
|
|
||||||
|
|
||||||
if (autoViewport) {
|
|
||||||
_scale.resetViewportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Reset rangeband and step size when we port over config
|
|
||||||
//scale.rangeBandConfig = get range band config
|
|
||||||
//scale.stepSizeConfig = get step size config
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double getLocation(D domain) => domain != null ? _scale[domain] : null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
D getDomain(double location) => _scale.reverse(location);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int compareDomainValueToViewport(D domain) {
|
|
||||||
return _scale.compareDomainValueToViewport(domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
void setOutputRange(int start, int end) {
|
|
||||||
_scale.range = ScaleOutputExtent(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request update ticks from tick provider and update the painted ticks.
|
|
||||||
void updateTicks() {
|
|
||||||
_updateProvidedTicks();
|
|
||||||
_updateAxisTicks();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request ticks from tick provider.
|
|
||||||
void _updateProvidedTicks() {
|
|
||||||
if (lockAxis) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Ensure that tick providers take manually configured
|
|
||||||
// viewport settings into account, so that we still get the right number.
|
|
||||||
_providedTicks = tickProvider.getTicks(
|
|
||||||
context: context,
|
|
||||||
graphicsFactory: graphicsFactory,
|
|
||||||
scale: _scale,
|
|
||||||
formatter: tickFormatter,
|
|
||||||
formatterValueCache: _formatterValueCache,
|
|
||||||
tickDrawStrategy: tickDrawStrategy,
|
|
||||||
orientation: axisOrientation,
|
|
||||||
viewportExtensionEnabled: autoViewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the ticks that are actually used for drawing.
|
|
||||||
void _updateAxisTicks() {
|
|
||||||
if (lockAxis) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final providedTicks = List.from(_providedTicks ?? []);
|
|
||||||
|
|
||||||
for (AxisTicks<D> animatedTick in _axisTicks) {
|
|
||||||
final tick = providedTicks?.firstWhere(
|
|
||||||
(t) => t.value == animatedTick.value,
|
|
||||||
orElse: () => null);
|
|
||||||
|
|
||||||
if (tick != null) {
|
|
||||||
// Swap out the text element only if the settings are different.
|
|
||||||
// This prevents a costly new TextPainter in Flutter.
|
|
||||||
if (!TextElement.elementSettingsSame(
|
|
||||||
animatedTick.textElement, tick.textElement)) {
|
|
||||||
animatedTick.textElement = tick.textElement;
|
|
||||||
}
|
|
||||||
// Update target for all existing ticks
|
|
||||||
animatedTick.setNewTarget(_scale[tick.value]);
|
|
||||||
providedTicks.remove(tick);
|
|
||||||
} else {
|
|
||||||
// Animate out ticks that do not exist any more.
|
|
||||||
animatedTick.animateOut(_scale[animatedTick.value].toDouble());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new ticks
|
|
||||||
providedTicks?.forEach((tick) {
|
|
||||||
final animatedTick = AxisTicks<D>(tick);
|
|
||||||
if (_previousScale != null) {
|
|
||||||
animatedTick.animateInFrom(_previousScale[tick.value].toDouble());
|
|
||||||
}
|
|
||||||
_axisTicks.add(animatedTick);
|
|
||||||
});
|
|
||||||
|
|
||||||
_axisTicks.sort();
|
|
||||||
|
|
||||||
// Save a copy of the current scale to be used as the previous scale when
|
|
||||||
// ticks are updated.
|
|
||||||
_previousScale = _scale.copy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configures the zoom and translate.
|
|
||||||
///
|
|
||||||
/// [viewportScale] is the zoom factor to use, likely >= 1.0 where 1.0 maps
|
|
||||||
/// the complete data extents to the output range, and 2.0 only maps half the
|
|
||||||
/// data to the output range.
|
|
||||||
///
|
|
||||||
/// [viewportTranslatePx] is the translate/pan to use in pixel units,
|
|
||||||
/// likely <= 0 which shifts the start of the data before the edge of the
|
|
||||||
/// chart giving us a pan.
|
|
||||||
///
|
|
||||||
/// [drawAreaWidth] is the width of the draw area for the series data in pixel
|
|
||||||
/// units, at minimum viewport scale level (1.0). When provided,
|
|
||||||
/// [viewportTranslatePx] will be clamped such that the axis cannot be panned
|
|
||||||
/// beyond the bounds of the data.
|
|
||||||
void setViewportSettings(double viewportScale, double viewportTranslatePx,
|
|
||||||
{int drawAreaWidth}) {
|
|
||||||
// Don't let the viewport be panned beyond the bounds of the data.
|
|
||||||
viewportTranslatePx = _clampTranslatePx(viewportScale, viewportTranslatePx,
|
|
||||||
drawAreaWidth: drawAreaWidth);
|
|
||||||
|
|
||||||
_scale.setViewportSettings(viewportScale, viewportTranslatePx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the current viewport scale.
|
|
||||||
///
|
|
||||||
/// A scale of 1.0 would map the data directly to the output range, while a
|
|
||||||
/// value of 2.0 would map the data to an output of double the range so you
|
|
||||||
/// only see half the data in the viewport. This is the equivalent to
|
|
||||||
/// zooming. Its value is likely >= 1.0.
|
|
||||||
double get viewportScalingFactor => _scale.viewportScalingFactor;
|
|
||||||
|
|
||||||
/// Returns the current pixel viewport offset
|
|
||||||
///
|
|
||||||
/// The translate is used by the scale function when it applies the scale.
|
|
||||||
/// This is the equivalent to panning. Its value is likely <= 0 to pan the
|
|
||||||
/// data to the left.
|
|
||||||
double get viewportTranslatePx => _scale?.viewportTranslatePx;
|
|
||||||
|
|
||||||
/// Clamps a possible change in domain translation to fit within the range of
|
|
||||||
/// the data.
|
|
||||||
double _clampTranslatePx(
|
|
||||||
double viewportScalingFactor, double viewportTranslatePx,
|
|
||||||
{int drawAreaWidth}) {
|
|
||||||
if (drawAreaWidth == null) {
|
|
||||||
return viewportTranslatePx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bound the viewport translate to the range of the data.
|
|
||||||
final maxNegativeTranslate =
|
|
||||||
-1.0 * ((drawAreaWidth * viewportScalingFactor) - drawAreaWidth);
|
|
||||||
|
|
||||||
viewportTranslatePx =
|
|
||||||
min(max(viewportTranslatePx, maxNegativeTranslate), 0.0);
|
|
||||||
|
|
||||||
return viewportTranslatePx;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// LayoutView methods.
|
|
||||||
//
|
|
||||||
|
|
||||||
@override
|
|
||||||
LayoutViewConfig get layoutConfig => LayoutViewConfig(
|
|
||||||
paintOrder: layoutPaintOrder,
|
|
||||||
position: _layoutPosition,
|
|
||||||
positionOrder: LayoutViewPositionOrder.axis);
|
|
||||||
|
|
||||||
/// Get layout position from axis orientation.
|
|
||||||
LayoutPosition get _layoutPosition {
|
|
||||||
LayoutPosition position;
|
|
||||||
switch (axisOrientation) {
|
|
||||||
case AxisOrientation.top:
|
|
||||||
position = LayoutPosition.Top;
|
|
||||||
break;
|
|
||||||
case AxisOrientation.right:
|
|
||||||
position = LayoutPosition.Right;
|
|
||||||
break;
|
|
||||||
case AxisOrientation.bottom:
|
|
||||||
position = LayoutPosition.Bottom;
|
|
||||||
break;
|
|
||||||
case AxisOrientation.left:
|
|
||||||
position = LayoutPosition.Left;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The axis is rendered vertically.
|
|
||||||
bool get isVertical =>
|
|
||||||
axisOrientation == AxisOrientation.left ||
|
|
||||||
axisOrientation == AxisOrientation.right;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
|
|
||||||
return isVertical
|
|
||||||
? _measureVerticalAxis(maxWidth, maxHeight)
|
|
||||||
: _measureHorizontalAxis(maxWidth, maxHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewMeasuredSizes _measureVerticalAxis(int maxWidth, int maxHeight) {
|
|
||||||
setOutputRange(maxHeight, 0);
|
|
||||||
_updateProvidedTicks();
|
|
||||||
|
|
||||||
return tickDrawStrategy.measureVerticallyDrawnTicks(
|
|
||||||
_providedTicks, maxWidth, maxHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewMeasuredSizes _measureHorizontalAxis(int maxWidth, int maxHeight) {
|
|
||||||
setOutputRange(0, maxWidth);
|
|
||||||
_updateProvidedTicks();
|
|
||||||
|
|
||||||
return tickDrawStrategy.measureHorizontallyDrawnTicks(
|
|
||||||
_providedTicks, maxWidth, maxHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout this component.
|
|
||||||
@override
|
|
||||||
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
|
|
||||||
_componentBounds = componentBounds;
|
|
||||||
_drawAreaBounds = drawAreaBounds;
|
|
||||||
|
|
||||||
// Update the output range if it is different than the current one.
|
|
||||||
// This is necessary because during the measure cycle, the output range is
|
|
||||||
// set between zero and the max range available. On layout, the output range
|
|
||||||
// needs to be updated to account of the offset of the axis view.
|
|
||||||
|
|
||||||
final outputStart =
|
|
||||||
isVertical ? _componentBounds.bottom : _componentBounds.left;
|
|
||||||
final outputEnd =
|
|
||||||
isVertical ? _componentBounds.top : _componentBounds.right;
|
|
||||||
|
|
||||||
final outputRange = reverseOutputRange
|
|
||||||
? ScaleOutputExtent(outputEnd, outputStart)
|
|
||||||
: ScaleOutputExtent(outputStart, outputEnd);
|
|
||||||
|
|
||||||
if (_scale.range != outputRange) {
|
|
||||||
_scale.range = outputRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateProvidedTicks();
|
|
||||||
// Update animated ticks in layout, because updateTicks are called during
|
|
||||||
// measure and we don't want to update the animation at that time.
|
|
||||||
_updateAxisTicks();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isSeriesRenderer => false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Rectangle<int> get componentBounds => this._componentBounds;
|
|
||||||
|
|
||||||
bool get drawAxisLine {
|
|
||||||
if (forceDrawAxisLine != null) {
|
|
||||||
return forceDrawAxisLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tickDrawStrategy is SmallTickDrawStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(ChartCanvas canvas, double animationPercent) {
|
|
||||||
if (animationPercent == 1.0) {
|
|
||||||
_axisTicks.removeWhere((t) => t.markedForRemoval);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < _axisTicks.length; i++) {
|
|
||||||
final animatedTick = _axisTicks[i];
|
|
||||||
tickDrawStrategy.draw(
|
|
||||||
canvas, animatedTick..setCurrentTick(animationPercent),
|
|
||||||
orientation: axisOrientation,
|
|
||||||
axisBounds: _componentBounds,
|
|
||||||
drawAreaBounds: _drawAreaBounds,
|
|
||||||
isFirst: i == 0,
|
|
||||||
isLast: i == _axisTicks.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (drawAxisLine) {
|
|
||||||
tickDrawStrategy.drawAxisLine(canvas, axisOrientation, _componentBounds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NumericAxis extends Axis<num> {
|
|
||||||
NumericAxis({TickProvider<num> tickProvider})
|
|
||||||
: super(
|
|
||||||
tickProvider: tickProvider ?? NumericTickProvider(),
|
|
||||||
tickFormatter: NumericTickFormatter(),
|
|
||||||
scale: LinearScale(),
|
|
||||||
);
|
|
||||||
|
|
||||||
void setScaleViewport(NumericExtents viewport) {
|
|
||||||
autoViewport = false;
|
|
||||||
(_scale as NumericScale).viewportDomain = viewport;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrdinalAxis extends Axis<String> {
|
|
||||||
OrdinalAxis({
|
|
||||||
TickDrawStrategy tickDrawStrategy,
|
|
||||||
TickProvider tickProvider,
|
|
||||||
TickFormatter tickFormatter,
|
|
||||||
}) : super(
|
|
||||||
tickProvider: tickProvider ?? const OrdinalTickProvider(),
|
|
||||||
tickFormatter: tickFormatter ?? const OrdinalTickFormatter(),
|
|
||||||
scale: SimpleOrdinalScale(),
|
|
||||||
);
|
|
||||||
|
|
||||||
void setScaleViewport(OrdinalViewport viewport) {
|
|
||||||
autoViewport = false;
|
|
||||||
(_scale as SimpleOrdinalScale)
|
|
||||||
.setViewport(viewport.dataSize, viewport.startingDomain);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
|
|
||||||
super.layout(componentBounds, drawAreaBounds);
|
|
||||||
|
|
||||||
// We are purposely clearing the viewport starting domain and data size
|
|
||||||
// post layout.
|
|
||||||
//
|
|
||||||
// Originally we set a flag in [setScaleViewport] to recalculate viewport
|
|
||||||
// settings on next scale update and then reset the flag. This doesn't work
|
|
||||||
// because chart's measure cycle provides different ranges to the scale,
|
|
||||||
// causing the scale to update multiple times before it is finalized after
|
|
||||||
// layout.
|
|
||||||
//
|
|
||||||
// By resetting the viewport after layout, we guarantee the correct range
|
|
||||||
// was used to apply the viewport and behaviors that update the viewport
|
|
||||||
// based on translate and scale changes will not be affected (pan/zoom).
|
|
||||||
(_scale as SimpleOrdinalScale).setViewport(null, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Viewport to cover [dataSize] data points starting at [startingDomain] value.
|
|
||||||
class OrdinalViewport {
|
|
||||||
final String startingDomain;
|
|
||||||
final int dataSize;
|
|
||||||
|
|
||||||
OrdinalViewport(this.startingDomain, this.dataSize);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is OrdinalViewport &&
|
|
||||||
startingDomain == other.startingDomain &&
|
|
||||||
dataSize == other.dataSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = startingDomain.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + dataSize;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
class AxisTester<D> {
|
|
||||||
final Axis<D> _axis;
|
|
||||||
|
|
||||||
AxisTester(this._axis);
|
|
||||||
|
|
||||||
List<AxisTicks<D>> get axisTicks => _axis._axisTicks;
|
|
||||||
|
|
||||||
MutableScale<D> get scale => _axis._scale;
|
|
||||||
|
|
||||||
List<D> get axisValues => axisTicks.map((t) => t.value).toList();
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'tick.dart' show Tick;
|
|
||||||
|
|
||||||
class AxisTicks<D> extends Tick<D> implements Comparable<AxisTicks<D>> {
|
|
||||||
/// This tick is being animated out.
|
|
||||||
bool _markedForRemoval;
|
|
||||||
|
|
||||||
/// This tick's current location.
|
|
||||||
double _currentLocation;
|
|
||||||
|
|
||||||
/// This tick's previous target location.
|
|
||||||
double _previousLocation;
|
|
||||||
|
|
||||||
/// This tick's current target location.
|
|
||||||
double _targetLocation;
|
|
||||||
|
|
||||||
/// This tick's current opacity.
|
|
||||||
double _currentOpacity;
|
|
||||||
|
|
||||||
/// This tick's previous opacity.
|
|
||||||
double _previousOpacity;
|
|
||||||
|
|
||||||
/// This tick's target opacity.
|
|
||||||
double _targetOpacity;
|
|
||||||
|
|
||||||
AxisTicks(Tick<D> tick)
|
|
||||||
: super(
|
|
||||||
value: tick.value,
|
|
||||||
textElement: tick.textElement,
|
|
||||||
locationPx: tick.locationPx,
|
|
||||||
labelOffsetPx: tick.labelOffsetPx) {
|
|
||||||
/// Set the initial target for a new animated tick.
|
|
||||||
_markedForRemoval = false;
|
|
||||||
_targetLocation = tick.locationPx;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get markedForRemoval => _markedForRemoval;
|
|
||||||
|
|
||||||
/// Animate the tick in from [previousLocation].
|
|
||||||
void animateInFrom(double previousLocation) {
|
|
||||||
_markedForRemoval = false;
|
|
||||||
_previousLocation = previousLocation;
|
|
||||||
_previousOpacity = 0.0;
|
|
||||||
_targetOpacity = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animate out this tick to [newLocation].
|
|
||||||
void animateOut(double newLocation) {
|
|
||||||
_markedForRemoval = true;
|
|
||||||
_previousLocation = _currentLocation;
|
|
||||||
_targetLocation = newLocation;
|
|
||||||
_previousOpacity = _currentOpacity;
|
|
||||||
_targetOpacity = 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set new target for this tick to be [newLocation].
|
|
||||||
void setNewTarget(double newLocation) {
|
|
||||||
_markedForRemoval = false;
|
|
||||||
_previousLocation = _currentLocation;
|
|
||||||
_targetLocation = newLocation;
|
|
||||||
_previousOpacity = _currentOpacity;
|
|
||||||
_targetOpacity = 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update tick's location and opacity based on animation percent.
|
|
||||||
void setCurrentTick(double animationPercent) {
|
|
||||||
if (animationPercent == 1.0) {
|
|
||||||
_currentLocation = _targetLocation;
|
|
||||||
_previousLocation = _targetLocation;
|
|
||||||
_currentOpacity = markedForRemoval ? 0.0 : 1.0;
|
|
||||||
} else if (_previousLocation == null) {
|
|
||||||
_currentLocation = _targetLocation;
|
|
||||||
_currentOpacity = 1.0;
|
|
||||||
} else {
|
|
||||||
_currentLocation =
|
|
||||||
_lerpDouble(_previousLocation, _targetLocation, animationPercent);
|
|
||||||
_currentOpacity =
|
|
||||||
_lerpDouble(_previousOpacity, _targetOpacity, animationPercent);
|
|
||||||
}
|
|
||||||
|
|
||||||
locationPx = _currentLocation;
|
|
||||||
textElement.opacity = _currentOpacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Linearly interpolate between two numbers.
|
|
||||||
///
|
|
||||||
/// From lerpDouble in dart:ui which is Flutter only.
|
|
||||||
double _lerpDouble(double a, double b, double t) {
|
|
||||||
if (a == null && b == null) return null;
|
|
||||||
a ??= 0.0;
|
|
||||||
b ??= 0.0;
|
|
||||||
return a + (b - a) * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
int compareTo(AxisTicks<D> other) {
|
|
||||||
return _targetLocation.compareTo(other._targetLocation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
import 'tick.dart' show Tick;
|
|
||||||
|
|
||||||
/// A report that contains a list of ticks and if they collide.
|
|
||||||
class CollisionReport {
|
|
||||||
/// If [ticks] collide.
|
|
||||||
final bool ticksCollide;
|
|
||||||
|
|
||||||
final List<Tick> ticks;
|
|
||||||
|
|
||||||
final bool alternateTicksUsed;
|
|
||||||
|
|
||||||
CollisionReport(
|
|
||||||
{@required this.ticksCollide,
|
|
||||||
@required this.ticks,
|
|
||||||
bool alternateTicksUsed})
|
|
||||||
: alternateTicksUsed = alternateTicksUsed ?? false;
|
|
||||||
|
|
||||||
CollisionReport.empty()
|
|
||||||
: ticksCollide = false,
|
|
||||||
ticks = [],
|
|
||||||
alternateTicksUsed = false;
|
|
||||||
}
|
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show immutable, protected, required;
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../../common/line_style.dart' show LineStyle;
|
|
||||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
|
||||||
import '../../../../common/text_element.dart' show TextDirection;
|
|
||||||
import '../../../../common/text_style.dart' show TextStyle;
|
|
||||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
|
|
||||||
import '../axis.dart' show AxisOrientation;
|
|
||||||
import '../collision_report.dart' show CollisionReport;
|
|
||||||
import '../spec/axis_spec.dart'
|
|
||||||
show
|
|
||||||
TextStyleSpec,
|
|
||||||
TickLabelAnchor,
|
|
||||||
TickLabelJustification,
|
|
||||||
LineStyleSpec,
|
|
||||||
RenderSpec;
|
|
||||||
import '../tick.dart' show Tick;
|
|
||||||
import 'tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
abstract class BaseRenderSpec<D> implements RenderSpec<D> {
|
|
||||||
final TextStyleSpec labelStyle;
|
|
||||||
final TickLabelAnchor labelAnchor;
|
|
||||||
final TickLabelJustification labelJustification;
|
|
||||||
|
|
||||||
final int labelOffsetFromAxisPx;
|
|
||||||
|
|
||||||
/// Absolute distance from the tick to the text if using start/end
|
|
||||||
final int labelOffsetFromTickPx;
|
|
||||||
|
|
||||||
final int minimumPaddingBetweenLabelsPx;
|
|
||||||
|
|
||||||
final LineStyleSpec axisLineStyle;
|
|
||||||
|
|
||||||
const BaseRenderSpec({
|
|
||||||
this.labelStyle,
|
|
||||||
this.labelAnchor,
|
|
||||||
this.labelJustification,
|
|
||||||
this.labelOffsetFromAxisPx,
|
|
||||||
this.labelOffsetFromTickPx,
|
|
||||||
this.minimumPaddingBetweenLabelsPx,
|
|
||||||
this.axisLineStyle,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other is BaseRenderSpec &&
|
|
||||||
labelStyle == other.labelStyle &&
|
|
||||||
labelAnchor == other.labelAnchor &&
|
|
||||||
labelJustification == other.labelJustification &&
|
|
||||||
labelOffsetFromTickPx == other.labelOffsetFromTickPx &&
|
|
||||||
labelOffsetFromAxisPx == other.labelOffsetFromAxisPx &&
|
|
||||||
minimumPaddingBetweenLabelsPx ==
|
|
||||||
other.minimumPaddingBetweenLabelsPx &&
|
|
||||||
axisLineStyle == other.axisLineStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = labelStyle?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + labelAnchor?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + labelJustification?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + labelOffsetFromTickPx?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + labelOffsetFromAxisPx?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + minimumPaddingBetweenLabelsPx?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + axisLineStyle?.hashCode ?? 0;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Base strategy that draws tick labels and checks for label collisions.
|
|
||||||
abstract class BaseTickDrawStrategy<D> implements TickDrawStrategy<D> {
|
|
||||||
final ChartContext chartContext;
|
|
||||||
|
|
||||||
LineStyle axisLineStyle;
|
|
||||||
TextStyle labelStyle;
|
|
||||||
TickLabelAnchor tickLabelAnchor;
|
|
||||||
TickLabelJustification tickLabelJustification;
|
|
||||||
int labelOffsetFromAxisPx;
|
|
||||||
int labelOffsetFromTickPx;
|
|
||||||
|
|
||||||
int minimumPaddingBetweenLabelsPx;
|
|
||||||
|
|
||||||
BaseTickDrawStrategy(this.chartContext, GraphicsFactory graphicsFactory,
|
|
||||||
{TextStyleSpec labelStyleSpec,
|
|
||||||
LineStyleSpec axisLineStyleSpec,
|
|
||||||
TickLabelAnchor labelAnchor,
|
|
||||||
TickLabelJustification labelJustification,
|
|
||||||
int labelOffsetFromAxisPx,
|
|
||||||
int labelOffsetFromTickPx,
|
|
||||||
int minimumPaddingBetweenLabelsPx}) {
|
|
||||||
labelStyle = (graphicsFactory.createTextPaint()
|
|
||||||
..color = labelStyleSpec?.color ?? StyleFactory.style.tickColor
|
|
||||||
..fontFamily = labelStyleSpec?.fontFamily
|
|
||||||
..fontSize = labelStyleSpec?.fontSize ?? 12);
|
|
||||||
|
|
||||||
axisLineStyle = graphicsFactory.createLinePaint()
|
|
||||||
..color = axisLineStyleSpec?.color ?? labelStyle.color
|
|
||||||
..dashPattern = axisLineStyleSpec?.dashPattern
|
|
||||||
..strokeWidth = axisLineStyleSpec?.thickness ?? 1;
|
|
||||||
|
|
||||||
tickLabelAnchor = labelAnchor ?? TickLabelAnchor.centered;
|
|
||||||
tickLabelJustification =
|
|
||||||
labelJustification ?? TickLabelJustification.inside;
|
|
||||||
this.labelOffsetFromAxisPx = labelOffsetFromAxisPx ?? 5;
|
|
||||||
this.labelOffsetFromTickPx = labelOffsetFromTickPx ?? 5;
|
|
||||||
this.minimumPaddingBetweenLabelsPx = minimumPaddingBetweenLabelsPx ?? 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void decorateTicks(List<Tick<D>> ticks) {
|
|
||||||
for (Tick<D> tick in ticks) {
|
|
||||||
// If no style at all, set the default style.
|
|
||||||
if (tick.textElement.textStyle == null) {
|
|
||||||
tick.textElement.textStyle = labelStyle;
|
|
||||||
} else {
|
|
||||||
//Fill in whatever is missing
|
|
||||||
tick.textElement.textStyle.color ??= labelStyle.color;
|
|
||||||
tick.textElement.textStyle.fontFamily ??= labelStyle.fontFamily;
|
|
||||||
tick.textElement.textStyle.fontSize ??= labelStyle.fontSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
CollisionReport collides(List<Tick<D>> ticks, AxisOrientation orientation) {
|
|
||||||
// If there are no ticks, they do not collide.
|
|
||||||
if (ticks == null) {
|
|
||||||
return CollisionReport(
|
|
||||||
ticksCollide: false, ticks: ticks, alternateTicksUsed: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
final vertical = orientation == AxisOrientation.left ||
|
|
||||||
orientation == AxisOrientation.right;
|
|
||||||
|
|
||||||
// First sort ticks by smallest locationPx first (NOT sorted by value).
|
|
||||||
// This allows us to only check if a tick collides with the previous tick.
|
|
||||||
ticks.sort((a, b) {
|
|
||||||
if (a.locationPx < b.locationPx) {
|
|
||||||
return -1;
|
|
||||||
} else if (a.locationPx > b.locationPx) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
double previousEnd = double.negativeInfinity;
|
|
||||||
bool collides = false;
|
|
||||||
|
|
||||||
for (final tick in ticks) {
|
|
||||||
final tickSize = tick.textElement.measurement;
|
|
||||||
|
|
||||||
if (vertical) {
|
|
||||||
final adjustedHeight =
|
|
||||||
tickSize.verticalSliceWidth + minimumPaddingBetweenLabelsPx;
|
|
||||||
|
|
||||||
if (tickLabelAnchor == TickLabelAnchor.inside) {
|
|
||||||
if (identical(tick, ticks.first)) {
|
|
||||||
// Top most tick draws down from the location
|
|
||||||
collides = false;
|
|
||||||
previousEnd = tick.locationPx + adjustedHeight;
|
|
||||||
} else if (identical(tick, ticks.last)) {
|
|
||||||
// Bottom most tick draws up from the location
|
|
||||||
collides = previousEnd > tick.locationPx - adjustedHeight;
|
|
||||||
previousEnd = tick.locationPx;
|
|
||||||
} else {
|
|
||||||
// All other ticks is centered.
|
|
||||||
final halfHeight = adjustedHeight / 2;
|
|
||||||
collides = previousEnd > tick.locationPx - halfHeight;
|
|
||||||
previousEnd = tick.locationPx + halfHeight;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
collides = previousEnd > tick.locationPx;
|
|
||||||
previousEnd = tick.locationPx + adjustedHeight;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use the text direction the ticks specified, unless the label anchor
|
|
||||||
// is set to [TickLabelAnchor.inside]. When 'inside' is set, the text
|
|
||||||
// direction is normalized such that the left most tick is drawn ltr,
|
|
||||||
// the last tick is drawn rtl, and all other ticks are in the center.
|
|
||||||
// This is not set until it is painted, so collision check needs to get
|
|
||||||
// the value also.
|
|
||||||
final textDirection = _normalizeHorizontalAnchor(
|
|
||||||
tickLabelAnchor,
|
|
||||||
chartContext.isRtl,
|
|
||||||
identical(tick, ticks.first),
|
|
||||||
identical(tick, ticks.last));
|
|
||||||
final adjustedWidth =
|
|
||||||
tickSize.horizontalSliceWidth + minimumPaddingBetweenLabelsPx;
|
|
||||||
switch (textDirection) {
|
|
||||||
case TextDirection.ltr:
|
|
||||||
collides = previousEnd > tick.locationPx;
|
|
||||||
previousEnd = tick.locationPx + adjustedWidth;
|
|
||||||
break;
|
|
||||||
case TextDirection.rtl:
|
|
||||||
collides = previousEnd > (tick.locationPx - adjustedWidth);
|
|
||||||
previousEnd = tick.locationPx;
|
|
||||||
break;
|
|
||||||
case TextDirection.center:
|
|
||||||
final halfWidth = adjustedWidth / 2;
|
|
||||||
collides = previousEnd > tick.locationPx - halfWidth;
|
|
||||||
previousEnd = tick.locationPx + halfWidth;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (collides) {
|
|
||||||
return CollisionReport(
|
|
||||||
ticksCollide: true, ticks: ticks, alternateTicksUsed: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return CollisionReport(
|
|
||||||
ticksCollide: false, ticks: ticks, alternateTicksUsed: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewMeasuredSizes measureVerticallyDrawnTicks(
|
|
||||||
List<Tick<D>> ticks, int maxWidth, int maxHeight) {
|
|
||||||
// TODO: Add spacing to account for the distance between the
|
|
||||||
// text and the axis baseline (even if it isn't drawn).
|
|
||||||
final maxHorizontalSliceWidth = ticks
|
|
||||||
.fold(
|
|
||||||
0.0,
|
|
||||||
(prevMax, tick) => max<double>(
|
|
||||||
prevMax,
|
|
||||||
tick.textElement.measurement.horizontalSliceWidth +
|
|
||||||
labelOffsetFromAxisPx))
|
|
||||||
.round();
|
|
||||||
|
|
||||||
return ViewMeasuredSizes(
|
|
||||||
preferredWidth: maxHorizontalSliceWidth, preferredHeight: maxHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewMeasuredSizes measureHorizontallyDrawnTicks(
|
|
||||||
List<Tick<D>> ticks, int maxWidth, int maxHeight) {
|
|
||||||
final maxVerticalSliceWidth = ticks
|
|
||||||
.fold(
|
|
||||||
0.0,
|
|
||||||
(prevMax, tick) => max<double>(
|
|
||||||
prevMax, tick.textElement.measurement.verticalSliceWidth))
|
|
||||||
.round();
|
|
||||||
|
|
||||||
return ViewMeasuredSizes(
|
|
||||||
preferredWidth: maxWidth,
|
|
||||||
preferredHeight: maxVerticalSliceWidth + labelOffsetFromAxisPx);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
|
|
||||||
Rectangle<int> axisBounds) {
|
|
||||||
Point<num> start;
|
|
||||||
Point<num> end;
|
|
||||||
|
|
||||||
switch (orientation) {
|
|
||||||
case AxisOrientation.top:
|
|
||||||
start = axisBounds.bottomLeft;
|
|
||||||
end = axisBounds.bottomRight;
|
|
||||||
break;
|
|
||||||
case AxisOrientation.bottom:
|
|
||||||
start = axisBounds.topLeft;
|
|
||||||
end = axisBounds.topRight;
|
|
||||||
break;
|
|
||||||
case AxisOrientation.right:
|
|
||||||
start = axisBounds.topLeft;
|
|
||||||
end = axisBounds.bottomLeft;
|
|
||||||
break;
|
|
||||||
case AxisOrientation.left:
|
|
||||||
start = axisBounds.topRight;
|
|
||||||
end = axisBounds.bottomRight;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawLine(
|
|
||||||
points: [start, end],
|
|
||||||
fill: axisLineStyle.color,
|
|
||||||
stroke: axisLineStyle.color,
|
|
||||||
strokeWidthPx: axisLineStyle.strokeWidth.toDouble(),
|
|
||||||
dashPattern: axisLineStyle.dashPattern,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void drawLabel(ChartCanvas canvas, Tick<D> tick,
|
|
||||||
{@required AxisOrientation orientation,
|
|
||||||
@required Rectangle<int> axisBounds,
|
|
||||||
@required Rectangle<int> drawAreaBounds,
|
|
||||||
@required bool isFirst,
|
|
||||||
@required bool isLast}) {
|
|
||||||
final locationPx = tick.locationPx;
|
|
||||||
final measurement = tick.textElement.measurement;
|
|
||||||
final isRtl = chartContext.isRtl;
|
|
||||||
|
|
||||||
int x = 0;
|
|
||||||
int y = 0;
|
|
||||||
|
|
||||||
final labelOffsetPx = tick.labelOffsetPx ?? 0;
|
|
||||||
|
|
||||||
if (orientation == AxisOrientation.bottom ||
|
|
||||||
orientation == AxisOrientation.top) {
|
|
||||||
y = orientation == AxisOrientation.bottom
|
|
||||||
? axisBounds.top + labelOffsetFromAxisPx
|
|
||||||
: axisBounds.bottom -
|
|
||||||
measurement.verticalSliceWidth.toInt() -
|
|
||||||
labelOffsetFromAxisPx;
|
|
||||||
|
|
||||||
final direction =
|
|
||||||
_normalizeHorizontalAnchor(tickLabelAnchor, isRtl, isFirst, isLast);
|
|
||||||
tick.textElement.textDirection = direction;
|
|
||||||
|
|
||||||
switch (direction) {
|
|
||||||
case TextDirection.rtl:
|
|
||||||
x = (locationPx + labelOffsetFromTickPx + labelOffsetPx).toInt();
|
|
||||||
break;
|
|
||||||
case TextDirection.ltr:
|
|
||||||
x = (locationPx - labelOffsetFromTickPx - labelOffsetPx).toInt();
|
|
||||||
break;
|
|
||||||
case TextDirection.center:
|
|
||||||
default:
|
|
||||||
x = (locationPx - labelOffsetPx).toInt();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (orientation == AxisOrientation.left) {
|
|
||||||
if (tickLabelJustification == TickLabelJustification.inside) {
|
|
||||||
x = axisBounds.right - labelOffsetFromAxisPx;
|
|
||||||
tick.textElement.textDirection = TextDirection.rtl;
|
|
||||||
} else {
|
|
||||||
x = axisBounds.left + labelOffsetFromAxisPx;
|
|
||||||
tick.textElement.textDirection = TextDirection.ltr;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// orientation == right
|
|
||||||
if (tickLabelJustification == TickLabelJustification.inside) {
|
|
||||||
x = axisBounds.left + labelOffsetFromAxisPx;
|
|
||||||
tick.textElement.textDirection = TextDirection.ltr;
|
|
||||||
} else {
|
|
||||||
x = axisBounds.right - labelOffsetFromAxisPx;
|
|
||||||
tick.textElement.textDirection = TextDirection.rtl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (_normalizeVerticalAnchor(tickLabelAnchor, isFirst, isLast)) {
|
|
||||||
case _PixelVerticalDirection.over:
|
|
||||||
y = (locationPx -
|
|
||||||
measurement.verticalSliceWidth -
|
|
||||||
labelOffsetFromTickPx -
|
|
||||||
labelOffsetPx)
|
|
||||||
.toInt();
|
|
||||||
break;
|
|
||||||
case _PixelVerticalDirection.under:
|
|
||||||
y = (locationPx + labelOffsetFromTickPx + labelOffsetPx).toInt();
|
|
||||||
break;
|
|
||||||
case _PixelVerticalDirection.center:
|
|
||||||
default:
|
|
||||||
y = (locationPx - measurement.verticalSliceWidth / 2 + labelOffsetPx)
|
|
||||||
.toInt();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawText(tick.textElement, x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextDirection _normalizeHorizontalAnchor(
|
|
||||||
TickLabelAnchor anchor, bool isRtl, bool isFirst, bool isLast) {
|
|
||||||
switch (anchor) {
|
|
||||||
case TickLabelAnchor.before:
|
|
||||||
return isRtl ? TextDirection.ltr : TextDirection.rtl;
|
|
||||||
case TickLabelAnchor.after:
|
|
||||||
return isRtl ? TextDirection.rtl : TextDirection.ltr;
|
|
||||||
case TickLabelAnchor.inside:
|
|
||||||
if (isFirst) {
|
|
||||||
return TextDirection.ltr;
|
|
||||||
}
|
|
||||||
if (isLast) {
|
|
||||||
return TextDirection.rtl;
|
|
||||||
}
|
|
||||||
return TextDirection.center;
|
|
||||||
case TickLabelAnchor.centered:
|
|
||||||
default:
|
|
||||||
return TextDirection.center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_PixelVerticalDirection _normalizeVerticalAnchor(
|
|
||||||
TickLabelAnchor anchor, bool isFirst, bool isLast) {
|
|
||||||
switch (anchor) {
|
|
||||||
case TickLabelAnchor.before:
|
|
||||||
return _PixelVerticalDirection.under;
|
|
||||||
case TickLabelAnchor.after:
|
|
||||||
return _PixelVerticalDirection.over;
|
|
||||||
case TickLabelAnchor.inside:
|
|
||||||
if (isFirst) {
|
|
||||||
return _PixelVerticalDirection.over;
|
|
||||||
}
|
|
||||||
if (isLast) {
|
|
||||||
return _PixelVerticalDirection.under;
|
|
||||||
}
|
|
||||||
return _PixelVerticalDirection.center;
|
|
||||||
case TickLabelAnchor.centered:
|
|
||||||
default:
|
|
||||||
return _PixelVerticalDirection.center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum _PixelVerticalDirection {
|
|
||||||
over,
|
|
||||||
center,
|
|
||||||
under,
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show immutable, required;
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../../common/line_style.dart' show LineStyle;
|
|
||||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
|
||||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../axis.dart' show AxisOrientation;
|
|
||||||
import '../spec/axis_spec.dart'
|
|
||||||
show TextStyleSpec, LineStyleSpec, TickLabelAnchor, TickLabelJustification;
|
|
||||||
import '../tick.dart' show Tick;
|
|
||||||
import 'base_tick_draw_strategy.dart' show BaseTickDrawStrategy;
|
|
||||||
import 'small_tick_draw_strategy.dart' show SmallTickRendererSpec;
|
|
||||||
import 'tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class GridlineRendererSpec<D> extends SmallTickRendererSpec<D> {
|
|
||||||
const GridlineRendererSpec({
|
|
||||||
TextStyleSpec labelStyle,
|
|
||||||
LineStyleSpec lineStyle,
|
|
||||||
LineStyleSpec axisLineStyle,
|
|
||||||
TickLabelAnchor labelAnchor,
|
|
||||||
TickLabelJustification labelJustification,
|
|
||||||
int tickLengthPx,
|
|
||||||
int labelOffsetFromAxisPx,
|
|
||||||
int labelOffsetFromTickPx,
|
|
||||||
int minimumPaddingBetweenLabelsPx,
|
|
||||||
}) : super(
|
|
||||||
labelStyle: labelStyle,
|
|
||||||
lineStyle: lineStyle,
|
|
||||||
labelAnchor: labelAnchor,
|
|
||||||
labelJustification: labelJustification,
|
|
||||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
|
||||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
|
||||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx,
|
|
||||||
tickLengthPx: tickLengthPx,
|
|
||||||
axisLineStyle: axisLineStyle);
|
|
||||||
|
|
||||||
@override
|
|
||||||
TickDrawStrategy<D> createDrawStrategy(
|
|
||||||
ChartContext context, GraphicsFactory graphicsFactory) =>
|
|
||||||
GridlineTickDrawStrategy<D>(context, graphicsFactory,
|
|
||||||
tickLengthPx: tickLengthPx,
|
|
||||||
lineStyleSpec: lineStyle,
|
|
||||||
labelStyleSpec: labelStyle,
|
|
||||||
axisLineStyleSpec: axisLineStyle,
|
|
||||||
labelAnchor: labelAnchor,
|
|
||||||
labelJustification: labelJustification,
|
|
||||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
|
||||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
|
||||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other is GridlineRendererSpec && super == (other));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = super.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws line across chart draw area for each tick.
|
|
||||||
///
|
|
||||||
/// Extends [BaseTickDrawStrategy].
|
|
||||||
class GridlineTickDrawStrategy<D> extends BaseTickDrawStrategy<D> {
|
|
||||||
int tickLength;
|
|
||||||
LineStyle lineStyle;
|
|
||||||
|
|
||||||
GridlineTickDrawStrategy(
|
|
||||||
ChartContext chartContext,
|
|
||||||
GraphicsFactory graphicsFactory, {
|
|
||||||
int tickLengthPx,
|
|
||||||
LineStyleSpec lineStyleSpec,
|
|
||||||
TextStyleSpec labelStyleSpec,
|
|
||||||
LineStyleSpec axisLineStyleSpec,
|
|
||||||
TickLabelAnchor labelAnchor,
|
|
||||||
TickLabelJustification labelJustification,
|
|
||||||
int labelOffsetFromAxisPx,
|
|
||||||
int labelOffsetFromTickPx,
|
|
||||||
int minimumPaddingBetweenLabelsPx,
|
|
||||||
}) : super(chartContext, graphicsFactory,
|
|
||||||
labelStyleSpec: labelStyleSpec,
|
|
||||||
axisLineStyleSpec: axisLineStyleSpec ?? lineStyleSpec,
|
|
||||||
labelAnchor: labelAnchor,
|
|
||||||
labelJustification: labelJustification,
|
|
||||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
|
||||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
|
||||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx) {
|
|
||||||
lineStyle =
|
|
||||||
StyleFactory.style.createGridlineStyle(graphicsFactory, lineStyleSpec);
|
|
||||||
|
|
||||||
this.tickLength = tickLengthPx ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void draw(ChartCanvas canvas, Tick<D> tick,
|
|
||||||
{@required AxisOrientation orientation,
|
|
||||||
@required Rectangle<int> axisBounds,
|
|
||||||
@required Rectangle<int> drawAreaBounds,
|
|
||||||
@required bool isFirst,
|
|
||||||
@required bool isLast}) {
|
|
||||||
Point<num> lineStart;
|
|
||||||
Point<num> lineEnd;
|
|
||||||
switch (orientation) {
|
|
||||||
case AxisOrientation.top:
|
|
||||||
final x = tick.locationPx;
|
|
||||||
lineStart = Point(x, axisBounds.bottom - tickLength);
|
|
||||||
lineEnd = Point(x, drawAreaBounds.bottom);
|
|
||||||
break;
|
|
||||||
case AxisOrientation.bottom:
|
|
||||||
final x = tick.locationPx;
|
|
||||||
lineStart = Point(x, drawAreaBounds.top + tickLength);
|
|
||||||
lineEnd = Point(x, axisBounds.top);
|
|
||||||
break;
|
|
||||||
case AxisOrientation.right:
|
|
||||||
final y = tick.locationPx;
|
|
||||||
if (tickLabelAnchor == TickLabelAnchor.after ||
|
|
||||||
tickLabelAnchor == TickLabelAnchor.before) {
|
|
||||||
lineStart = Point(axisBounds.right, y);
|
|
||||||
} else {
|
|
||||||
lineStart = Point(axisBounds.left + tickLength, y);
|
|
||||||
}
|
|
||||||
lineEnd = Point(drawAreaBounds.left, y);
|
|
||||||
break;
|
|
||||||
case AxisOrientation.left:
|
|
||||||
final y = tick.locationPx;
|
|
||||||
|
|
||||||
if (tickLabelAnchor == TickLabelAnchor.after ||
|
|
||||||
tickLabelAnchor == TickLabelAnchor.before) {
|
|
||||||
lineStart = Point(axisBounds.left, y);
|
|
||||||
} else {
|
|
||||||
lineStart = Point(axisBounds.right - tickLength, y);
|
|
||||||
}
|
|
||||||
lineEnd = Point(drawAreaBounds.right, y);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawLine(
|
|
||||||
points: [lineStart, lineEnd],
|
|
||||||
dashPattern: lineStyle.dashPattern,
|
|
||||||
fill: lineStyle.color,
|
|
||||||
stroke: lineStyle.color,
|
|
||||||
strokeWidthPx: lineStyle.strokeWidth.toDouble(),
|
|
||||||
);
|
|
||||||
|
|
||||||
drawLabel(canvas, tick,
|
|
||||||
orientation: orientation,
|
|
||||||
axisBounds: axisBounds,
|
|
||||||
drawAreaBounds: drawAreaBounds,
|
|
||||||
isFirst: isFirst,
|
|
||||||
isLast: isLast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show immutable, required;
|
|
||||||
|
|
||||||
import '../../../../common/color.dart' show Color;
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../../common/line_style.dart' show LineStyle;
|
|
||||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
|
||||||
import '../../../../common/text_style.dart' show TextStyle;
|
|
||||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
|
|
||||||
import '../axis.dart' show AxisOrientation;
|
|
||||||
import '../collision_report.dart' show CollisionReport;
|
|
||||||
import '../spec/axis_spec.dart' show RenderSpec, LineStyleSpec;
|
|
||||||
import '../tick.dart' show Tick;
|
|
||||||
import 'tick_draw_strategy.dart';
|
|
||||||
|
|
||||||
/// Renders no ticks no labels, and claims no space in layout.
|
|
||||||
/// However, it does render the axis line if asked to by the axis.
|
|
||||||
@immutable
|
|
||||||
class NoneRenderSpec<D> extends RenderSpec<D> {
|
|
||||||
final LineStyleSpec axisLineStyle;
|
|
||||||
|
|
||||||
const NoneRenderSpec({this.axisLineStyle});
|
|
||||||
|
|
||||||
@override
|
|
||||||
TickDrawStrategy<D> createDrawStrategy(
|
|
||||||
ChartContext context, GraphicsFactory graphicFactory) =>
|
|
||||||
NoneDrawStrategy<D>(context, graphicFactory,
|
|
||||||
axisLineStyleSpec: axisLineStyle);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) || other is NoneRenderSpec;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoneDrawStrategy<D> implements TickDrawStrategy<D> {
|
|
||||||
LineStyle axisLineStyle;
|
|
||||||
TextStyle noneTextStyle;
|
|
||||||
|
|
||||||
NoneDrawStrategy(ChartContext chartContext, GraphicsFactory graphicsFactory,
|
|
||||||
{LineStyleSpec axisLineStyleSpec}) {
|
|
||||||
axisLineStyle = StyleFactory.style
|
|
||||||
.createAxisLineStyle(graphicsFactory, axisLineStyleSpec);
|
|
||||||
noneTextStyle = graphicsFactory.createTextPaint()
|
|
||||||
..color = Color.transparent
|
|
||||||
..fontSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
CollisionReport collides(List<Tick> ticks, AxisOrientation orientation) =>
|
|
||||||
CollisionReport(ticksCollide: false, ticks: ticks);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void decorateTicks(List<Tick> ticks) {
|
|
||||||
// Even though no text is rendered, the text style for each element should
|
|
||||||
// still be set to handle the case of the draw strategy being switched to
|
|
||||||
// a different draw strategy. The new draw strategy will try to animate
|
|
||||||
// the old ticks out and the text style property is used.
|
|
||||||
ticks.forEach((tick) => tick.textElement.textStyle = noneTextStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
|
|
||||||
Rectangle<int> axisBounds) {
|
|
||||||
Point<num> start;
|
|
||||||
Point<num> end;
|
|
||||||
|
|
||||||
switch (orientation) {
|
|
||||||
case AxisOrientation.top:
|
|
||||||
start = axisBounds.bottomLeft;
|
|
||||||
end = axisBounds.bottomRight;
|
|
||||||
|
|
||||||
break;
|
|
||||||
case AxisOrientation.bottom:
|
|
||||||
start = axisBounds.topLeft;
|
|
||||||
end = axisBounds.topRight;
|
|
||||||
break;
|
|
||||||
case AxisOrientation.right:
|
|
||||||
start = axisBounds.topLeft;
|
|
||||||
end = axisBounds.bottomLeft;
|
|
||||||
break;
|
|
||||||
case AxisOrientation.left:
|
|
||||||
start = axisBounds.topRight;
|
|
||||||
end = axisBounds.bottomRight;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawLine(
|
|
||||||
points: [start, end],
|
|
||||||
dashPattern: axisLineStyle.dashPattern,
|
|
||||||
fill: axisLineStyle.color,
|
|
||||||
stroke: axisLineStyle.color,
|
|
||||||
strokeWidthPx: axisLineStyle.strokeWidth.toDouble(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void draw(ChartCanvas canvas, Tick<D> tick,
|
|
||||||
{@required AxisOrientation orientation,
|
|
||||||
@required Rectangle<int> axisBounds,
|
|
||||||
@required Rectangle<int> drawAreaBounds,
|
|
||||||
@required bool isFirst,
|
|
||||||
@required bool isLast}) {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewMeasuredSizes measureHorizontallyDrawnTicks(
|
|
||||||
List<Tick> ticks, int maxWidth, int maxHeight) {
|
|
||||||
return ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewMeasuredSizes measureVerticallyDrawnTicks(
|
|
||||||
List<Tick> ticks, int maxWidth, int maxHeight) {
|
|
||||||
return ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show immutable, required;
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../../common/line_style.dart' show LineStyle;
|
|
||||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
|
||||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../axis.dart' show AxisOrientation;
|
|
||||||
import '../spec/axis_spec.dart'
|
|
||||||
show TextStyleSpec, LineStyleSpec, TickLabelAnchor, TickLabelJustification;
|
|
||||||
import '../tick.dart' show Tick;
|
|
||||||
import 'base_tick_draw_strategy.dart' show BaseRenderSpec, BaseTickDrawStrategy;
|
|
||||||
import 'tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
|
|
||||||
///
|
|
||||||
@immutable
|
|
||||||
class SmallTickRendererSpec<D> extends BaseRenderSpec<D> {
|
|
||||||
final LineStyleSpec lineStyle;
|
|
||||||
final int tickLengthPx;
|
|
||||||
|
|
||||||
const SmallTickRendererSpec({
|
|
||||||
TextStyleSpec labelStyle,
|
|
||||||
this.lineStyle,
|
|
||||||
LineStyleSpec axisLineStyle,
|
|
||||||
TickLabelAnchor labelAnchor,
|
|
||||||
TickLabelJustification labelJustification,
|
|
||||||
int labelOffsetFromAxisPx,
|
|
||||||
int labelOffsetFromTickPx,
|
|
||||||
this.tickLengthPx,
|
|
||||||
int minimumPaddingBetweenLabelsPx,
|
|
||||||
}) : super(
|
|
||||||
labelStyle: labelStyle,
|
|
||||||
labelAnchor: labelAnchor,
|
|
||||||
labelJustification: labelJustification,
|
|
||||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
|
||||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
|
||||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx,
|
|
||||||
axisLineStyle: axisLineStyle);
|
|
||||||
|
|
||||||
@override
|
|
||||||
TickDrawStrategy<D> createDrawStrategy(
|
|
||||||
ChartContext context, GraphicsFactory graphicsFactory) =>
|
|
||||||
SmallTickDrawStrategy<D>(context, graphicsFactory,
|
|
||||||
tickLengthPx: tickLengthPx,
|
|
||||||
lineStyleSpec: lineStyle,
|
|
||||||
labelStyleSpec: labelStyle,
|
|
||||||
axisLineStyleSpec: axisLineStyle,
|
|
||||||
labelAnchor: labelAnchor,
|
|
||||||
labelJustification: labelJustification,
|
|
||||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
|
||||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
|
||||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other is SmallTickRendererSpec &&
|
|
||||||
lineStyle == other.lineStyle &&
|
|
||||||
tickLengthPx == other.tickLengthPx &&
|
|
||||||
super == (other));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = lineStyle?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + tickLengthPx?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + super.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws small tick lines for each tick. Extends [BaseTickDrawStrategy].
|
|
||||||
class SmallTickDrawStrategy<D> extends BaseTickDrawStrategy<D> {
|
|
||||||
int tickLength;
|
|
||||||
LineStyle lineStyle;
|
|
||||||
|
|
||||||
SmallTickDrawStrategy(
|
|
||||||
ChartContext chartContext,
|
|
||||||
GraphicsFactory graphicsFactory, {
|
|
||||||
int tickLengthPx,
|
|
||||||
LineStyleSpec lineStyleSpec,
|
|
||||||
TextStyleSpec labelStyleSpec,
|
|
||||||
LineStyleSpec axisLineStyleSpec,
|
|
||||||
TickLabelAnchor labelAnchor,
|
|
||||||
TickLabelJustification labelJustification,
|
|
||||||
int labelOffsetFromAxisPx,
|
|
||||||
int labelOffsetFromTickPx,
|
|
||||||
int minimumPaddingBetweenLabelsPx,
|
|
||||||
}) : super(chartContext, graphicsFactory,
|
|
||||||
labelStyleSpec: labelStyleSpec,
|
|
||||||
axisLineStyleSpec: axisLineStyleSpec ?? lineStyleSpec,
|
|
||||||
labelAnchor: labelAnchor,
|
|
||||||
labelJustification: labelJustification,
|
|
||||||
labelOffsetFromAxisPx: labelOffsetFromAxisPx,
|
|
||||||
labelOffsetFromTickPx: labelOffsetFromTickPx,
|
|
||||||
minimumPaddingBetweenLabelsPx: minimumPaddingBetweenLabelsPx) {
|
|
||||||
this.tickLength = tickLengthPx ?? StyleFactory.style.tickLength;
|
|
||||||
lineStyle =
|
|
||||||
StyleFactory.style.createTickLineStyle(graphicsFactory, lineStyleSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void draw(ChartCanvas canvas, Tick<D> tick,
|
|
||||||
{@required AxisOrientation orientation,
|
|
||||||
@required Rectangle<int> axisBounds,
|
|
||||||
@required Rectangle<int> drawAreaBounds,
|
|
||||||
@required bool isFirst,
|
|
||||||
@required bool isLast}) {
|
|
||||||
Point<num> tickStart;
|
|
||||||
Point<num> tickEnd;
|
|
||||||
switch (orientation) {
|
|
||||||
case AxisOrientation.top:
|
|
||||||
double x = tick.locationPx;
|
|
||||||
tickStart = Point(x, axisBounds.bottom - tickLength);
|
|
||||||
tickEnd = Point(x, axisBounds.bottom);
|
|
||||||
break;
|
|
||||||
case AxisOrientation.bottom:
|
|
||||||
double x = tick.locationPx;
|
|
||||||
tickStart = Point(x, axisBounds.top);
|
|
||||||
tickEnd = Point(x, axisBounds.top + tickLength);
|
|
||||||
break;
|
|
||||||
case AxisOrientation.right:
|
|
||||||
double y = tick.locationPx;
|
|
||||||
|
|
||||||
tickStart = Point(axisBounds.left, y);
|
|
||||||
tickEnd = Point(axisBounds.left + tickLength, y);
|
|
||||||
break;
|
|
||||||
case AxisOrientation.left:
|
|
||||||
double y = tick.locationPx;
|
|
||||||
|
|
||||||
tickStart = Point(axisBounds.right - tickLength, y);
|
|
||||||
tickEnd = Point(axisBounds.right, y);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawLine(
|
|
||||||
points: [tickStart, tickEnd],
|
|
||||||
dashPattern: lineStyle.dashPattern,
|
|
||||||
fill: lineStyle.color,
|
|
||||||
stroke: lineStyle.color,
|
|
||||||
strokeWidthPx: lineStyle.strokeWidth.toDouble(),
|
|
||||||
);
|
|
||||||
|
|
||||||
drawLabel(canvas, tick,
|
|
||||||
orientation: orientation,
|
|
||||||
axisBounds: axisBounds,
|
|
||||||
drawAreaBounds: drawAreaBounds,
|
|
||||||
isFirst: isFirst,
|
|
||||||
isLast: isLast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../common/chart_canvas.dart' show ChartCanvas;
|
|
||||||
import '../../../layout/layout_view.dart' show ViewMeasuredSizes;
|
|
||||||
import '../axis.dart' show AxisOrientation;
|
|
||||||
import '../collision_report.dart' show CollisionReport;
|
|
||||||
import '../tick.dart' show Tick;
|
|
||||||
|
|
||||||
/// Strategy for drawing ticks and checking for collisions.
|
|
||||||
abstract class TickDrawStrategy<D> {
|
|
||||||
/// Decorate the existing list of ticks.
|
|
||||||
///
|
|
||||||
/// This can be used to further modify ticks after they have been generated
|
|
||||||
/// with location data and formatted labels.
|
|
||||||
void decorateTicks(List<Tick<D>> ticks);
|
|
||||||
|
|
||||||
/// Returns a [CollisionReport] indicating if there are any collisions.
|
|
||||||
CollisionReport collides(List<Tick<D>> ticks, AxisOrientation orientation);
|
|
||||||
|
|
||||||
/// Returns measurement of ticks drawn vertically.
|
|
||||||
ViewMeasuredSizes measureVerticallyDrawnTicks(
|
|
||||||
List<Tick<D>> ticks, int maxWidth, int maxHeight);
|
|
||||||
|
|
||||||
/// Returns measurement of ticks drawn horizontally.
|
|
||||||
ViewMeasuredSizes measureHorizontallyDrawnTicks(
|
|
||||||
List<Tick<D>> ticks, int maxWidth, int maxHeight);
|
|
||||||
|
|
||||||
/// Draws tick onto [ChartCanvas].
|
|
||||||
///
|
|
||||||
/// [orientation] the orientation of the axis that this [tick] belongs to.
|
|
||||||
/// [axisBounds] the bounds of the axis.
|
|
||||||
/// [drawAreaBounds] the bounds of the chart draw area adjacent to the axis.
|
|
||||||
void draw(ChartCanvas canvas, Tick<D> tick,
|
|
||||||
{@required AxisOrientation orientation,
|
|
||||||
@required Rectangle<int> axisBounds,
|
|
||||||
@required Rectangle<int> drawAreaBounds,
|
|
||||||
@required bool isFirst,
|
|
||||||
@required bool isLast});
|
|
||||||
|
|
||||||
void drawAxisLine(ChartCanvas canvas, AxisOrientation orientation,
|
|
||||||
Rectangle<int> axisBounds);
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../common/chart_context.dart' show ChartContext;
|
|
||||||
import 'axis.dart' show AxisOrientation;
|
|
||||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import 'numeric_scale.dart' show NumericScale;
|
|
||||||
import 'ordinal_scale.dart' show OrdinalScale;
|
|
||||||
import 'scale.dart' show MutableScale;
|
|
||||||
import 'tick.dart' show Tick;
|
|
||||||
import 'tick_formatter.dart' show TickFormatter;
|
|
||||||
import 'tick_provider.dart' show BaseTickProvider, TickHint;
|
|
||||||
import 'time/date_time_scale.dart' show DateTimeScale;
|
|
||||||
|
|
||||||
/// Tick provider that provides ticks at the two end points of the axis range.
|
|
||||||
class EndPointsTickProvider<D> extends BaseTickProvider<D> {
|
|
||||||
@override
|
|
||||||
List<Tick<D>> getTicks({
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required MutableScale<D> scale,
|
|
||||||
@required TickFormatter<D> formatter,
|
|
||||||
@required Map<D, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
@required AxisOrientation orientation,
|
|
||||||
bool viewportExtensionEnabled = false,
|
|
||||||
TickHint<D> tickHint,
|
|
||||||
}) {
|
|
||||||
final ticks = <Tick<D>>[];
|
|
||||||
|
|
||||||
// Check to see if the axis has been configured with some domain values.
|
|
||||||
//
|
|
||||||
// An un-configured axis has no domain step size, and its scale defaults to
|
|
||||||
// infinity.
|
|
||||||
if (scale.domainStepSize.abs() != double.infinity) {
|
|
||||||
final start = _getStartValue(tickHint, scale);
|
|
||||||
final end = _getEndValue(tickHint, scale);
|
|
||||||
|
|
||||||
final labels = formatter.format([start, end], formatterValueCache,
|
|
||||||
stepSize: scale.domainStepSize);
|
|
||||||
|
|
||||||
ticks.add(Tick(
|
|
||||||
value: start,
|
|
||||||
textElement: graphicsFactory.createTextElement(labels[0]),
|
|
||||||
locationPx: scale[start]));
|
|
||||||
|
|
||||||
ticks.add(Tick(
|
|
||||||
value: end,
|
|
||||||
textElement: graphicsFactory.createTextElement(labels[1]),
|
|
||||||
locationPx: scale[end]));
|
|
||||||
|
|
||||||
// Allow draw strategy to decorate the ticks.
|
|
||||||
tickDrawStrategy.decorateTicks(ticks);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ticks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the start value from the scale.
|
|
||||||
D _getStartValue(TickHint<D> tickHint, MutableScale<D> scale) {
|
|
||||||
Object start;
|
|
||||||
|
|
||||||
if (tickHint != null) {
|
|
||||||
start = tickHint.start;
|
|
||||||
} else {
|
|
||||||
if (scale is NumericScale) {
|
|
||||||
start = (scale as NumericScale).viewportDomain.min;
|
|
||||||
} else if (scale is DateTimeScale) {
|
|
||||||
start = (scale as DateTimeScale).viewportDomain.start;
|
|
||||||
} else if (scale is OrdinalScale) {
|
|
||||||
start = (scale as OrdinalScale).domain.first;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the end value from the scale.
|
|
||||||
D _getEndValue(TickHint<D> tickHint, MutableScale<D> scale) {
|
|
||||||
Object end;
|
|
||||||
|
|
||||||
if (tickHint != null) {
|
|
||||||
end = tickHint.end;
|
|
||||||
} else {
|
|
||||||
if (scale is NumericScale) {
|
|
||||||
end = (scale as NumericScale).viewportDomain.max;
|
|
||||||
} else if (scale is DateTimeScale) {
|
|
||||||
end = (scale as DateTimeScale).viewportDomain.end;
|
|
||||||
} else if (scale is OrdinalScale) {
|
|
||||||
end = (scale as OrdinalScale).domain.last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../axis.dart' show NumericAxis;
|
|
||||||
import 'bucketing_numeric_tick_provider.dart' show BucketingNumericTickProvider;
|
|
||||||
|
|
||||||
/// A numeric [Axis] that positions all values beneath a certain [threshold]
|
|
||||||
/// into a reserved space on the axis range. The label for the bucket line will
|
|
||||||
/// be drawn in the middle of the bucket range, rather than aligned with the
|
|
||||||
/// gridline for that value's position on the scale.
|
|
||||||
///
|
|
||||||
/// An example illustration of a bucketing measure axis on a point chart
|
|
||||||
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
|
|
||||||
/// of the axis, since they are less than the [threshold] value of 10%.
|
|
||||||
///
|
|
||||||
/// 100% ┠─────────────────────────
|
|
||||||
/// ┃ *
|
|
||||||
/// ┃ *
|
|
||||||
/// 50% ┠──────*──────────────────
|
|
||||||
/// ┃
|
|
||||||
/// ┠─────────────────────────
|
|
||||||
/// < 10% ┃ * *
|
|
||||||
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
|
|
||||||
/// 0 50 100
|
|
||||||
///
|
|
||||||
/// This axis will format numbers as percents by default.
|
|
||||||
class BucketingNumericAxis extends NumericAxis {
|
|
||||||
/// All values smaller than the threshold will be bucketed into the same
|
|
||||||
/// position in the reserved space on the axis.
|
|
||||||
num _threshold;
|
|
||||||
|
|
||||||
/// Whether or not measure values bucketed below the [threshold] should be
|
|
||||||
/// visible on the chart, or collapsed.
|
|
||||||
///
|
|
||||||
/// If this is false, then any data with measure values smaller than
|
|
||||||
/// [threshold] will be rendered at the baseline of the chart. The
|
|
||||||
bool _showBucket;
|
|
||||||
|
|
||||||
BucketingNumericAxis() : super(tickProvider: BucketingNumericTickProvider());
|
|
||||||
|
|
||||||
set threshold(num threshold) {
|
|
||||||
_threshold = threshold;
|
|
||||||
(tickProvider as BucketingNumericTickProvider).threshold = threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
set showBucket(bool showBucket) {
|
|
||||||
_showBucket = showBucket;
|
|
||||||
(tickProvider as BucketingNumericTickProvider).showBucket = showBucket;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the location of [domain] on the axis, repositioning any value less
|
|
||||||
/// than [threshold] to the middle of the reserved bucket.
|
|
||||||
@override
|
|
||||||
double getLocation(num domain) {
|
|
||||||
if (domain == null) {
|
|
||||||
return null;
|
|
||||||
} else if (_threshold != null && domain < _threshold) {
|
|
||||||
return _showBucket ? scale[_threshold / 2] : scale[0.0];
|
|
||||||
} else {
|
|
||||||
return scale[domain];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../axis.dart' show AxisOrientation;
|
|
||||||
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import '../numeric_scale.dart' show NumericScale;
|
|
||||||
import '../numeric_tick_provider.dart' show NumericTickProvider;
|
|
||||||
import '../tick.dart' show Tick;
|
|
||||||
import '../tick_formatter.dart' show SimpleTickFormatterBase, TickFormatter;
|
|
||||||
import '../tick_provider.dart' show TickHint;
|
|
||||||
|
|
||||||
/// Tick provider that generates ticks for a [BucketingNumericAxis].
|
|
||||||
///
|
|
||||||
/// An example illustration of a bucketing measure axis on a point chart
|
|
||||||
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
|
|
||||||
/// of the axis, since they are less than the [threshold] value of 10%.
|
|
||||||
///
|
|
||||||
/// 100% ┠─────────────────────────
|
|
||||||
/// ┃ *
|
|
||||||
/// ┃ *
|
|
||||||
/// 50% ┠──────*──────────────────
|
|
||||||
/// ┃
|
|
||||||
/// ┠─────────────────────────
|
|
||||||
/// < 10% ┃ * *
|
|
||||||
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
|
|
||||||
/// 0 50 100
|
|
||||||
///
|
|
||||||
/// This tick provider will generate ticks using the same strategy as
|
|
||||||
/// [NumericTickProvider], except that any ticks that are smaller than
|
|
||||||
/// [threshold] will be hidden with an empty label. A special tick will be added
|
|
||||||
/// at the [threshold] position, with a label offset that moves its label down
|
|
||||||
/// to the middle of the bucket.
|
|
||||||
class BucketingNumericTickProvider extends NumericTickProvider {
|
|
||||||
/// All values smaller than the threshold will be bucketed into the same
|
|
||||||
/// position in the reserved space on the axis.
|
|
||||||
num _threshold;
|
|
||||||
|
|
||||||
set threshold(num threshold) {
|
|
||||||
_threshold = threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not measure values bucketed below the [threshold] should be
|
|
||||||
/// visible on the chart, or collapsed.
|
|
||||||
bool _showBucket;
|
|
||||||
|
|
||||||
set showBucket(bool showBucket) {
|
|
||||||
_showBucket = showBucket;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Tick<num>> getTicks({
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required NumericScale scale,
|
|
||||||
@required TickFormatter<num> formatter,
|
|
||||||
@required Map<num, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
@required AxisOrientation orientation,
|
|
||||||
bool viewportExtensionEnabled = false,
|
|
||||||
TickHint<num> tickHint,
|
|
||||||
}) {
|
|
||||||
if (_threshold == null) {
|
|
||||||
throw ('Bucketing threshold must be set before getting ticks.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_showBucket == null) {
|
|
||||||
throw ('The showBucket flag must be set before getting ticks.');
|
|
||||||
}
|
|
||||||
|
|
||||||
final localFormatter = _BucketingFormatter()
|
|
||||||
..threshold = _threshold
|
|
||||||
..originalFormatter = formatter;
|
|
||||||
|
|
||||||
final ticks = super.getTicks(
|
|
||||||
context: context,
|
|
||||||
graphicsFactory: graphicsFactory,
|
|
||||||
scale: scale,
|
|
||||||
formatter: localFormatter,
|
|
||||||
formatterValueCache: formatterValueCache,
|
|
||||||
tickDrawStrategy: tickDrawStrategy,
|
|
||||||
orientation: orientation,
|
|
||||||
viewportExtensionEnabled: viewportExtensionEnabled);
|
|
||||||
|
|
||||||
assert(scale != null);
|
|
||||||
|
|
||||||
// Create a tick for the threshold.
|
|
||||||
final thresholdTick = Tick<num>(
|
|
||||||
value: _threshold,
|
|
||||||
textElement: graphicsFactory
|
|
||||||
.createTextElement(localFormatter.formatValue(_threshold)),
|
|
||||||
locationPx: _showBucket ? scale[_threshold] : scale[0],
|
|
||||||
labelOffsetPx:
|
|
||||||
_showBucket ? -0.5 * (scale[_threshold] - scale[0]) : 0.0);
|
|
||||||
tickDrawStrategy.decorateTicks(<Tick<num>>[thresholdTick]);
|
|
||||||
|
|
||||||
// Filter out ticks that sit below the threshold.
|
|
||||||
ticks.removeWhere(
|
|
||||||
(tick) => tick.value <= thresholdTick.value && tick.value != 0.0);
|
|
||||||
|
|
||||||
// Finally, add our threshold tick to the list.
|
|
||||||
ticks.add(thresholdTick);
|
|
||||||
|
|
||||||
// Make sure they are sorted by increasing value.
|
|
||||||
ticks.sort((a, b) {
|
|
||||||
if (a.value < b.value) {
|
|
||||||
return -1;
|
|
||||||
} else if (a.value > b.value) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return ticks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BucketingFormatter extends SimpleTickFormatterBase<num> {
|
|
||||||
/// All values smaller than the threshold will be formatted into an empty
|
|
||||||
/// string.
|
|
||||||
num threshold;
|
|
||||||
|
|
||||||
SimpleTickFormatterBase<num> originalFormatter;
|
|
||||||
|
|
||||||
/// Formats a single tick value.
|
|
||||||
String formatValue(num value) {
|
|
||||||
if (value < threshold) {
|
|
||||||
return '';
|
|
||||||
} else if (value == threshold) {
|
|
||||||
return '< ' + originalFormatter.formatValue(value);
|
|
||||||
} else {
|
|
||||||
return originalFormatter.formatValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../numeric_extents.dart' show NumericExtents;
|
|
||||||
import '../numeric_scale.dart' show NumericScale;
|
|
||||||
import '../scale.dart' show RangeBandConfig, ScaleOutputExtent, StepSizeConfig;
|
|
||||||
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
|
|
||||||
import 'linear_scale_function.dart' show LinearScaleFunction;
|
|
||||||
import 'linear_scale_viewport.dart' show LinearScaleViewportSettings;
|
|
||||||
|
|
||||||
/// [NumericScale] that lays out the domain linearly across the range.
|
|
||||||
///
|
|
||||||
/// A [Scale] which converts numeric domain units to a given numeric range units
|
|
||||||
/// linearly (as opposed to other methods like log scales). This is used to map
|
|
||||||
/// the domain's values to the available pixel range of the chart using the
|
|
||||||
/// apply method.
|
|
||||||
///
|
|
||||||
/// <p>The domain extent of the scale are determined by adding all domain
|
|
||||||
/// values to the scale. It can, however, be overwritten by calling
|
|
||||||
/// [domainOverride] to define the extent of the data.
|
|
||||||
///
|
|
||||||
/// <p>The scale can be zoomed & panned by calling either [setViewportSettings]
|
|
||||||
/// with a zoom and translate, or by setting [viewportExtent] with the domain
|
|
||||||
/// extent to show in the output range.
|
|
||||||
///
|
|
||||||
/// <p>[rangeBandConfig]: By default, this scale will map the domain extent
|
|
||||||
/// exactly to the output range in a simple ratio mapping. If a
|
|
||||||
/// [RangeBandConfig] other than NONE is used to define the width of bar groups,
|
|
||||||
/// then the scale calculation may be altered to that there is a half a stepSize
|
|
||||||
/// at the start and end of the range to ensure that a bar group can be shown
|
|
||||||
/// and centered on the scale's result.
|
|
||||||
///
|
|
||||||
/// <p>[stepSizeConfig]: By default, this scale will calculate the stepSize as
|
|
||||||
/// being auto detected using the minimal distance between two consecutive
|
|
||||||
/// datum. If you don't assign a [RangeBandConfig], then changing the
|
|
||||||
/// [stepSizeConfig] is a no-op.
|
|
||||||
class LinearScale implements NumericScale {
|
|
||||||
final LinearScaleDomainInfo _domainInfo;
|
|
||||||
final LinearScaleViewportSettings _viewportSettings;
|
|
||||||
final LinearScaleFunction _scaleFunction = LinearScaleFunction();
|
|
||||||
|
|
||||||
RangeBandConfig rangeBandConfig = const RangeBandConfig.none();
|
|
||||||
StepSizeConfig stepSizeConfig = const StepSizeConfig.auto();
|
|
||||||
|
|
||||||
bool _scaleReady = false;
|
|
||||||
|
|
||||||
LinearScale()
|
|
||||||
: _domainInfo = LinearScaleDomainInfo(),
|
|
||||||
_viewportSettings = LinearScaleViewportSettings();
|
|
||||||
|
|
||||||
LinearScale._copy(LinearScale other)
|
|
||||||
: _domainInfo = LinearScaleDomainInfo.copy(other._domainInfo),
|
|
||||||
_viewportSettings =
|
|
||||||
LinearScaleViewportSettings.copy(other._viewportSettings),
|
|
||||||
rangeBandConfig = other.rangeBandConfig,
|
|
||||||
stepSizeConfig = other.stepSizeConfig;
|
|
||||||
|
|
||||||
@override
|
|
||||||
LinearScale copy() => LinearScale._copy(this);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Domain methods
|
|
||||||
//
|
|
||||||
|
|
||||||
@override
|
|
||||||
addDomain(num domainValue) {
|
|
||||||
_domainInfo.addDomainValue(domainValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
resetDomain() {
|
|
||||||
_scaleReady = false;
|
|
||||||
_domainInfo.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
resetViewportSettings() {
|
|
||||||
_viewportSettings.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
NumericExtents get dataExtent =>
|
|
||||||
NumericExtents(_domainInfo.dataDomainStart, _domainInfo.dataDomainEnd);
|
|
||||||
|
|
||||||
@override
|
|
||||||
num get minimumDomainStep => _domainInfo.minimumDetectedDomainStep;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool canTranslate(_) => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set domainOverride(NumericExtents domainMaxExtent) {
|
|
||||||
_domainInfo.domainOverride = domainMaxExtent;
|
|
||||||
}
|
|
||||||
|
|
||||||
get domainOverride => _domainInfo.domainOverride;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int compareDomainValueToViewport(num domainValue) {
|
|
||||||
NumericExtents dataExtent = _viewportSettings.domainExtent != null
|
|
||||||
? _viewportSettings.domainExtent
|
|
||||||
: _domainInfo.extent;
|
|
||||||
return dataExtent.compareValue(domainValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Viewport methods
|
|
||||||
//
|
|
||||||
|
|
||||||
@override
|
|
||||||
setViewportSettings(double viewportScale, double viewportTranslatePx) {
|
|
||||||
_viewportSettings
|
|
||||||
..scalingFactor = viewportScale
|
|
||||||
..translatePx = viewportTranslatePx
|
|
||||||
..domainExtent = null;
|
|
||||||
_scaleReady = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get viewportScalingFactor => _viewportSettings.scalingFactor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get viewportTranslatePx => _viewportSettings.translatePx;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set viewportDomain(NumericExtents extent) {
|
|
||||||
_scaleReady = false;
|
|
||||||
_viewportSettings.domainExtent = extent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
NumericExtents get viewportDomain {
|
|
||||||
_configureScale();
|
|
||||||
return _viewportSettings.domainExtent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
set keepViewportWithinData(bool autoAdjustViewportToNiceValues) {
|
|
||||||
_scaleReady = false;
|
|
||||||
_viewportSettings.keepViewportWithinData = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get keepViewportWithinData => _viewportSettings.keepViewportWithinData;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double computeViewportScaleFactor(double domainWindow) =>
|
|
||||||
_domainInfo.domainDiff / domainWindow;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set range(ScaleOutputExtent extent) {
|
|
||||||
_viewportSettings.range = extent;
|
|
||||||
_scaleReady = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ScaleOutputExtent get range => _viewportSettings.range;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Scale application methods
|
|
||||||
//
|
|
||||||
|
|
||||||
@override
|
|
||||||
num operator [](num domainValue) {
|
|
||||||
_configureScale();
|
|
||||||
return _scaleFunction[domainValue];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
num reverse(double viewPixels) {
|
|
||||||
_configureScale();
|
|
||||||
final num domain = _scaleFunction.reverse(viewPixels);
|
|
||||||
return domain;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get rangeBand {
|
|
||||||
_configureScale();
|
|
||||||
return _scaleFunction.rangeBandPixels;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get stepSize {
|
|
||||||
_configureScale();
|
|
||||||
return _scaleFunction.stepSizePixels;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get domainStepSize => _domainInfo.minimumDetectedDomainStep.toDouble();
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get rangeWidth => (range.end - range.start).abs().toInt();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isRangeValueWithinViewport(double rangeValue) =>
|
|
||||||
range.containsValue(rangeValue);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Private update
|
|
||||||
//
|
|
||||||
|
|
||||||
_configureScale() {
|
|
||||||
if (_scaleReady) return;
|
|
||||||
|
|
||||||
assert(_viewportSettings.range != null);
|
|
||||||
|
|
||||||
// If the viewport's domainExtent are set, then we can calculate the
|
|
||||||
// viewport's scaleFactor now that the domainInfo has been loaded.
|
|
||||||
// The viewport also has a chance to correct the scaleFactor.
|
|
||||||
_viewportSettings.updateViewportScaleFactor(_domainInfo);
|
|
||||||
// Now that the viewport's scalingFactor is setup, set it on the scale
|
|
||||||
// function.
|
|
||||||
_scaleFunction.updateScaleFactor(
|
|
||||||
_viewportSettings, _domainInfo, rangeBandConfig, stepSizeConfig);
|
|
||||||
|
|
||||||
// If the viewport's domainExtent are set, then we can calculate the
|
|
||||||
// viewport's translate now that the scaleFactor has been loaded.
|
|
||||||
// The viewport also has a chance to correct the translate.
|
|
||||||
_viewportSettings.updateViewportTranslatePx(
|
|
||||||
_domainInfo, _scaleFunction.scalingFactor);
|
|
||||||
// Now that the viewport has a chance to update the translate, set it on the
|
|
||||||
// scale function.
|
|
||||||
_scaleFunction.updateTranslateAndRangeBand(
|
|
||||||
_viewportSettings, _domainInfo, rangeBandConfig);
|
|
||||||
|
|
||||||
// Now that the viewport's scaleFactor and translate have been updated
|
|
||||||
// set the effective domainExtent of the viewport.
|
|
||||||
_viewportSettings.updateViewportDomainExtent(
|
|
||||||
_domainInfo, _scaleFunction.scalingFactor);
|
|
||||||
|
|
||||||
// Cached computed values are updated.
|
|
||||||
_scaleReady = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../numeric_extents.dart' show NumericExtents;
|
|
||||||
|
|
||||||
/// Encapsulation of all the domain processing logic for the [LinearScale].
|
|
||||||
class LinearScaleDomainInfo {
|
|
||||||
/// User (or axis) overridden extent in domain units.
|
|
||||||
NumericExtents domainOverride;
|
|
||||||
|
|
||||||
/// The minimum added domain value.
|
|
||||||
num _dataDomainStart = double.infinity;
|
|
||||||
num get dataDomainStart => _dataDomainStart;
|
|
||||||
|
|
||||||
/// The maximum added domain value.
|
|
||||||
num _dataDomainEnd = double.negativeInfinity;
|
|
||||||
num get dataDomainEnd => _dataDomainEnd;
|
|
||||||
|
|
||||||
/// Previous domain added so we can calculate minimumDetectedDomainStep.
|
|
||||||
num _previouslyAddedDomain;
|
|
||||||
|
|
||||||
/// The step size between data points in domain units.
|
|
||||||
///
|
|
||||||
/// Measured as the minimum distance between consecutive added points.
|
|
||||||
num _minimumDetectedDomainStep = double.infinity;
|
|
||||||
num get minimumDetectedDomainStep => _minimumDetectedDomainStep;
|
|
||||||
|
|
||||||
///The diff of the nicedDomain extent.
|
|
||||||
num get domainDiff => extent.width;
|
|
||||||
|
|
||||||
LinearScaleDomainInfo();
|
|
||||||
|
|
||||||
LinearScaleDomainInfo.copy(LinearScaleDomainInfo other) {
|
|
||||||
if (other.domainOverride != null) {
|
|
||||||
domainOverride = other.domainOverride;
|
|
||||||
}
|
|
||||||
_dataDomainStart = other._dataDomainStart;
|
|
||||||
_dataDomainEnd = other._dataDomainEnd;
|
|
||||||
_previouslyAddedDomain = other._previouslyAddedDomain;
|
|
||||||
_minimumDetectedDomainStep = other._minimumDetectedDomainStep;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets everything back to initial state.
|
|
||||||
void reset() {
|
|
||||||
_previouslyAddedDomain = null;
|
|
||||||
_dataDomainStart = double.infinity;
|
|
||||||
_dataDomainEnd = double.negativeInfinity;
|
|
||||||
_minimumDetectedDomainStep = double.infinity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the domain extent and detected step size given the [domainValue].
|
|
||||||
void addDomainValue(num domainValue) {
|
|
||||||
if (domainValue == null || !domainValue.isFinite) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
extendDomain(domainValue);
|
|
||||||
|
|
||||||
if (_previouslyAddedDomain != null) {
|
|
||||||
final domainStep = (domainValue - _previouslyAddedDomain).abs();
|
|
||||||
if (domainStep != 0.0 && domainStep < minimumDetectedDomainStep) {
|
|
||||||
_minimumDetectedDomainStep = domainStep;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_previouslyAddedDomain = domainValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extends the data domain extent without modifying step size detection.
|
|
||||||
///
|
|
||||||
/// Returns whether the the domain interval was extended. If the domain value
|
|
||||||
/// was already contained in the domain interval, the domain interval does not
|
|
||||||
/// change.
|
|
||||||
bool extendDomain(num domainValue) {
|
|
||||||
if (domainValue == null || !domainValue.isFinite) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool domainExtended = false;
|
|
||||||
if (domainValue < _dataDomainStart) {
|
|
||||||
_dataDomainStart = domainValue;
|
|
||||||
domainExtended = true;
|
|
||||||
}
|
|
||||||
if (domainValue > _dataDomainEnd) {
|
|
||||||
_dataDomainEnd = domainValue;
|
|
||||||
domainExtended = true;
|
|
||||||
}
|
|
||||||
return domainExtended;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the extent based on the current domain range and overrides.
|
|
||||||
NumericExtents get extent {
|
|
||||||
num tmpDomainStart;
|
|
||||||
num tmpDomainEnd;
|
|
||||||
if (domainOverride != null) {
|
|
||||||
// override was set.
|
|
||||||
tmpDomainStart = domainOverride.min;
|
|
||||||
tmpDomainEnd = domainOverride.max;
|
|
||||||
} else {
|
|
||||||
// domainEnd is less than domainStart if no domain values have been set.
|
|
||||||
tmpDomainStart = _dataDomainStart.isFinite ? _dataDomainStart : 0.0;
|
|
||||||
tmpDomainEnd = _dataDomainEnd.isFinite ? _dataDomainEnd : 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NumericExtents(tmpDomainStart, tmpDomainEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../scale.dart'
|
|
||||||
show RangeBandConfig, RangeBandType, StepSizeConfig, StepSizeType;
|
|
||||||
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
|
|
||||||
import 'linear_scale_viewport.dart' show LinearScaleViewportSettings;
|
|
||||||
|
|
||||||
/// Component of the LinearScale which actually handles the apply and reverse
|
|
||||||
/// function of the scale.
|
|
||||||
class LinearScaleFunction {
|
|
||||||
/// Cached rangeBand width in pixels given the RangeBandConfig and the current
|
|
||||||
/// domain & range.
|
|
||||||
double rangeBandPixels = 0.0;
|
|
||||||
|
|
||||||
/// Cached amount in domain units to shift the input value as a part of
|
|
||||||
/// translation.
|
|
||||||
num domainTranslate = 0.0;
|
|
||||||
|
|
||||||
/// Cached translation ratio for scale translation.
|
|
||||||
double scalingFactor = 1.0;
|
|
||||||
|
|
||||||
/// Cached amount in pixel units to shift the output value as a part of
|
|
||||||
/// translation.
|
|
||||||
double rangeTranslate = 0.0;
|
|
||||||
|
|
||||||
/// The calculated step size given the step size config.
|
|
||||||
double stepSizePixels = 0.0;
|
|
||||||
|
|
||||||
/// Translates the given domainValue to the range output.
|
|
||||||
double operator [](num domainValue) {
|
|
||||||
return (((domainValue + domainTranslate) * scalingFactor) + rangeTranslate)
|
|
||||||
.toDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Translates the given range output back to a domainValue.
|
|
||||||
double reverse(double viewPixels) {
|
|
||||||
return ((viewPixels - rangeTranslate) / scalingFactor) - domainTranslate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the scale function's scaleFactor given the current state of the
|
|
||||||
/// viewport.
|
|
||||||
void updateScaleFactor(
|
|
||||||
LinearScaleViewportSettings viewportSettings,
|
|
||||||
LinearScaleDomainInfo domainInfo,
|
|
||||||
RangeBandConfig rangeBandConfig,
|
|
||||||
StepSizeConfig stepSizeConfig) {
|
|
||||||
double rangeDiff = viewportSettings.range.diff.toDouble();
|
|
||||||
// Note: if you provided a nicing function that extends the domain, we won't
|
|
||||||
// muck with the extended side.
|
|
||||||
bool hasHalfStepAtStart =
|
|
||||||
domainInfo.extent.min == domainInfo.dataDomainStart;
|
|
||||||
bool hasHalfStepAtEnd = domainInfo.extent.max == domainInfo.dataDomainEnd;
|
|
||||||
|
|
||||||
// Determine the stepSize and reserved range values.
|
|
||||||
// The percentage of the step reserved from the scale's range due to the
|
|
||||||
// possible half step at the start and end.
|
|
||||||
double reservedRangePercentOfStep =
|
|
||||||
getStepReservationPercent(hasHalfStepAtStart, hasHalfStepAtEnd);
|
|
||||||
_updateStepSizeAndScaleFactor(viewportSettings, domainInfo, rangeDiff,
|
|
||||||
reservedRangePercentOfStep, rangeBandConfig, stepSizeConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the percentage of the step reserved from the output range due to
|
|
||||||
/// maybe having to hold half stepSizes on the start and end of the output.
|
|
||||||
double getStepReservationPercent(
|
|
||||||
bool hasHalfStepAtStart, bool hasHalfStepAtEnd) {
|
|
||||||
if (!hasHalfStepAtStart && !hasHalfStepAtEnd) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
if (hasHalfStepAtStart && hasHalfStepAtEnd) {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
return 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the scale function's translate and rangeBand given the current
|
|
||||||
/// state of the viewport.
|
|
||||||
void updateTranslateAndRangeBand(LinearScaleViewportSettings viewportSettings,
|
|
||||||
LinearScaleDomainInfo domainInfo, RangeBandConfig rangeBandConfig) {
|
|
||||||
// Assign the rangeTranslate using the current viewportSettings.translatePx
|
|
||||||
// and diffs.
|
|
||||||
if (domainInfo.domainDiff == 0) {
|
|
||||||
// Translate it to the center of the range.
|
|
||||||
rangeTranslate =
|
|
||||||
viewportSettings.range.start + (viewportSettings.range.diff / 2);
|
|
||||||
} else {
|
|
||||||
bool hasHalfStepAtStart =
|
|
||||||
domainInfo.extent.min == domainInfo.dataDomainStart;
|
|
||||||
// The pixel shift of the scale function due to the half a step at the
|
|
||||||
// beginning.
|
|
||||||
double reservedRangePixelShift =
|
|
||||||
hasHalfStepAtStart ? (stepSizePixels / 2.0) : 0.0;
|
|
||||||
|
|
||||||
rangeTranslate = (viewportSettings.range.start +
|
|
||||||
viewportSettings.translatePx +
|
|
||||||
reservedRangePixelShift);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to subtract the start from any incoming domain to apply the
|
|
||||||
// scale, so flip its sign.
|
|
||||||
domainTranslate = -1 * domainInfo.extent.min;
|
|
||||||
|
|
||||||
// Update the rangeBand size.
|
|
||||||
rangeBandPixels = _calculateRangeBandSize(rangeBandConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates and stores the current rangeBand given the config and current
|
|
||||||
/// step size.
|
|
||||||
double _calculateRangeBandSize(RangeBandConfig rangeBandConfig) {
|
|
||||||
switch (rangeBandConfig.type) {
|
|
||||||
case RangeBandType.fixedDomain:
|
|
||||||
return rangeBandConfig.size * scalingFactor;
|
|
||||||
case RangeBandType.fixedPixel:
|
|
||||||
return rangeBandConfig.size;
|
|
||||||
case RangeBandType.fixedPixelSpaceFromStep:
|
|
||||||
return stepSizePixels - rangeBandConfig.size;
|
|
||||||
case RangeBandType.styleAssignedPercentOfStep:
|
|
||||||
case RangeBandType.fixedPercentOfStep:
|
|
||||||
return stepSizePixels * rangeBandConfig.size;
|
|
||||||
case RangeBandType.none:
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates and Stores the current step size and scale factor together,
|
|
||||||
/// given the viewport, domain, and config.
|
|
||||||
///
|
|
||||||
/// <p>Scale factor and step size are related closely and should be calculated
|
|
||||||
/// together so that we do not lose accuracy due to double arithmetic.
|
|
||||||
void _updateStepSizeAndScaleFactor(
|
|
||||||
LinearScaleViewportSettings viewportSettings,
|
|
||||||
LinearScaleDomainInfo domainInfo,
|
|
||||||
double rangeDiff,
|
|
||||||
double reservedRangePercentOfStep,
|
|
||||||
RangeBandConfig rangeBandConfig,
|
|
||||||
StepSizeConfig stepSizeConfig) {
|
|
||||||
final domainDiff = domainInfo.domainDiff;
|
|
||||||
|
|
||||||
// If we are going to have any rangeBands, then ensure that we account for
|
|
||||||
// needed space on the beginning and end of the range.
|
|
||||||
if (rangeBandConfig.type != RangeBandType.none) {
|
|
||||||
switch (stepSizeConfig.type) {
|
|
||||||
case StepSizeType.autoDetect:
|
|
||||||
double minimumDetectedDomainStep =
|
|
||||||
domainInfo.minimumDetectedDomainStep.toDouble();
|
|
||||||
if (minimumDetectedDomainStep != null &&
|
|
||||||
minimumDetectedDomainStep.isFinite) {
|
|
||||||
scalingFactor = viewportSettings.scalingFactor *
|
|
||||||
(rangeDiff /
|
|
||||||
(domainDiff +
|
|
||||||
(minimumDetectedDomainStep *
|
|
||||||
reservedRangePercentOfStep)));
|
|
||||||
stepSizePixels = (minimumDetectedDomainStep * scalingFactor);
|
|
||||||
} else {
|
|
||||||
stepSizePixels = rangeDiff.abs();
|
|
||||||
scalingFactor = 1.0;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case StepSizeType.fixedPixels:
|
|
||||||
stepSizePixels = stepSizeConfig.size;
|
|
||||||
double reservedRangeForStepPixels =
|
|
||||||
stepSizePixels * reservedRangePercentOfStep;
|
|
||||||
scalingFactor = domainDiff == 0
|
|
||||||
? 1.0
|
|
||||||
: viewportSettings.scalingFactor *
|
|
||||||
(rangeDiff - reservedRangeForStepPixels) /
|
|
||||||
domainDiff;
|
|
||||||
return;
|
|
||||||
case StepSizeType.fixedDomain:
|
|
||||||
double domainStepWidth = stepSizeConfig.size;
|
|
||||||
double totalDomainDiff =
|
|
||||||
(domainDiff + (domainStepWidth * reservedRangePercentOfStep));
|
|
||||||
scalingFactor = totalDomainDiff == 0
|
|
||||||
? 1.0
|
|
||||||
: viewportSettings.scalingFactor * (rangeDiff / totalDomainDiff);
|
|
||||||
stepSizePixels = domainStepWidth * scalingFactor;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no cases matched, use zero step size.
|
|
||||||
stepSizePixels = 0.0;
|
|
||||||
scalingFactor = domainDiff == 0
|
|
||||||
? 1.0
|
|
||||||
: viewportSettings.scalingFactor * rangeDiff / domainDiff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' as math show max, min;
|
|
||||||
|
|
||||||
import '../numeric_extents.dart' show NumericExtents;
|
|
||||||
import '../scale.dart' show ScaleOutputExtent;
|
|
||||||
import 'linear_scale_domain_info.dart' show LinearScaleDomainInfo;
|
|
||||||
|
|
||||||
/// Component of the LinearScale responsible for the configuration and
|
|
||||||
/// calculations of the viewport.
|
|
||||||
class LinearScaleViewportSettings {
|
|
||||||
/// Output extent for the scale, typically set by the axis as the pixel
|
|
||||||
/// output.
|
|
||||||
ScaleOutputExtent range;
|
|
||||||
|
|
||||||
/// Determines whether the scale should be extended to the nice values
|
|
||||||
/// provided by the tick provider. If true, we wont touch the viewport config
|
|
||||||
/// since the axis will configure it, if false, we will still ensure sane zoom
|
|
||||||
/// and translates.
|
|
||||||
bool keepViewportWithinData = true;
|
|
||||||
|
|
||||||
/// User configured viewport scale as a zoom multiplier where 1.0 is
|
|
||||||
/// 100% (default) and 2.0 is 200% zooming in making the data take up twice
|
|
||||||
/// the space (showing half as much data in the viewport).
|
|
||||||
double scalingFactor = 1.0;
|
|
||||||
|
|
||||||
/// User configured viewport translate in pixel units.
|
|
||||||
double translatePx = 0.0;
|
|
||||||
|
|
||||||
/// The current extent of the viewport in domain units.
|
|
||||||
NumericExtents _domainExtent;
|
|
||||||
set domainExtent(NumericExtents extent) {
|
|
||||||
_domainExtent = extent;
|
|
||||||
_manualDomainExtent = extent != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
NumericExtents get domainExtent => _domainExtent;
|
|
||||||
|
|
||||||
/// Indicates that the viewportExtends are to be read from to determine the
|
|
||||||
/// internal scaleFactor and rangeTranslate.
|
|
||||||
|
|
||||||
bool _manualDomainExtent = false;
|
|
||||||
|
|
||||||
LinearScaleViewportSettings();
|
|
||||||
|
|
||||||
LinearScaleViewportSettings.copy(LinearScaleViewportSettings other) {
|
|
||||||
range = other.range;
|
|
||||||
keepViewportWithinData = other.keepViewportWithinData;
|
|
||||||
scalingFactor = other.scalingFactor;
|
|
||||||
translatePx = other.translatePx;
|
|
||||||
_manualDomainExtent = other._manualDomainExtent;
|
|
||||||
_domainExtent = other._domainExtent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the viewport calculated fields back to their initial settings.
|
|
||||||
void reset() {
|
|
||||||
// Likely an auto assigned viewport (niced), so reset it between draws.
|
|
||||||
scalingFactor = 1.0;
|
|
||||||
translatePx = 0.0;
|
|
||||||
domainExtent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
int get rangeWidth => range.diff.abs().toInt();
|
|
||||||
|
|
||||||
bool isRangeValueWithinViewport(double rangeValue) =>
|
|
||||||
range.containsValue(rangeValue);
|
|
||||||
|
|
||||||
/// Updates the viewport's internal scalingFactor given the current
|
|
||||||
/// domainInfo.
|
|
||||||
void updateViewportScaleFactor(LinearScaleDomainInfo domainInfo) {
|
|
||||||
// If we are loading from the viewport, then update the scalingFactor given
|
|
||||||
// the viewport size compared to the data size.
|
|
||||||
if (_manualDomainExtent) {
|
|
||||||
double viewportDomainDiff = _domainExtent?.width?.toDouble();
|
|
||||||
if (domainInfo.domainDiff != 0.0) {
|
|
||||||
scalingFactor = domainInfo.domainDiff / viewportDomainDiff;
|
|
||||||
} else {
|
|
||||||
scalingFactor = 1.0;
|
|
||||||
// The domain claims to have no date, extend it to the viewport's
|
|
||||||
domainInfo.extendDomain(_domainExtent?.min);
|
|
||||||
domainInfo.extendDomain(_domainExtent?.max);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure that the viewportSettings.scalingFactor is sane if desired.
|
|
||||||
if (!keepViewportWithinData) {
|
|
||||||
// Make sure we don't zoom out beyond the max domain extent.
|
|
||||||
scalingFactor = math.max(1.0, scalingFactor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the viewport's internal translate given the current domainInfo and
|
|
||||||
/// main scalingFactor from LinearScaleFunction (not internal scalingFactor).
|
|
||||||
void updateViewportTranslatePx(
|
|
||||||
LinearScaleDomainInfo domainInfo, double scaleScalingFactor) {
|
|
||||||
// If we are loading from the viewport, then update the translate now that
|
|
||||||
// the scaleFactor has been setup.
|
|
||||||
if (_manualDomainExtent) {
|
|
||||||
translatePx = (-1.0 *
|
|
||||||
scaleScalingFactor *
|
|
||||||
(_domainExtent.min - domainInfo.extent.min));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure that the viewportSettings.translatePx is sane if desired.
|
|
||||||
if (!keepViewportWithinData) {
|
|
||||||
int rangeDiff = range.diff.toInt();
|
|
||||||
|
|
||||||
// Make sure we don't translate beyond the max domain extent.
|
|
||||||
translatePx = math.min(0.0, translatePx);
|
|
||||||
translatePx = math.max(rangeDiff * (1.0 - scalingFactor), translatePx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates and stores the viewport's domainExtent if we did not load from
|
|
||||||
/// them in the first place.
|
|
||||||
void updateViewportDomainExtent(
|
|
||||||
LinearScaleDomainInfo domainInfo, double scaleScalingFactor) {
|
|
||||||
// If we didn't load from the viewport extent, then update them given the
|
|
||||||
// current scale configuration.
|
|
||||||
if (!_manualDomainExtent) {
|
|
||||||
double viewportDomainDiff = domainInfo.domainDiff / scalingFactor;
|
|
||||||
double viewportStart =
|
|
||||||
(-1.0 * translatePx / scaleScalingFactor) + domainInfo.extent.min;
|
|
||||||
_domainExtent =
|
|
||||||
NumericExtents(viewportStart, viewportStart + viewportDomainDiff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'scale.dart' show Extents;
|
|
||||||
|
|
||||||
/// Represents the starting and ending extent of a dataset.
|
|
||||||
class NumericExtents implements Extents<num> {
|
|
||||||
final num min;
|
|
||||||
final num max;
|
|
||||||
|
|
||||||
/// Precondition: [min] <= [max].
|
|
||||||
// TODO: When initializer list asserts are supported everywhere,
|
|
||||||
// add the precondition as an initializer list assert. This is supported in
|
|
||||||
// Flutter only.
|
|
||||||
const NumericExtents(this.min, this.max);
|
|
||||||
|
|
||||||
/// Returns [Extents] based on the min and max of the given values.
|
|
||||||
/// Returns [NumericExtents.empty] if [values] are empty
|
|
||||||
factory NumericExtents.fromValues(Iterable<num> values) {
|
|
||||||
if (values.isEmpty) {
|
|
||||||
return NumericExtents.empty;
|
|
||||||
}
|
|
||||||
var min = values.first;
|
|
||||||
var max = values.first;
|
|
||||||
for (final value in values) {
|
|
||||||
if (value < min) {
|
|
||||||
min = value;
|
|
||||||
} else if (max < value) {
|
|
||||||
max = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NumericExtents(min, max);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the union of this and other.
|
|
||||||
NumericExtents plus(NumericExtents other) {
|
|
||||||
if (min <= other.min) {
|
|
||||||
if (max >= other.max) {
|
|
||||||
return this;
|
|
||||||
} else {
|
|
||||||
return NumericExtents(min, other.max);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (other.max >= max) {
|
|
||||||
return other;
|
|
||||||
} else {
|
|
||||||
return NumericExtents(other.min, max);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compares the given [value] against the extents.
|
|
||||||
///
|
|
||||||
/// Returns -1 if the value is less than the extents.
|
|
||||||
/// Returns 0 if the value is within the extents inclusive.
|
|
||||||
/// Returns 1 if the value is greater than the extents.
|
|
||||||
int compareValue(num value) {
|
|
||||||
if (value < min) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (value > max) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _containsValue(double value) => compareValue(value) == 0;
|
|
||||||
|
|
||||||
// Returns true if these [NumericExtents] collides with [other].
|
|
||||||
bool overlaps(NumericExtents other) {
|
|
||||||
return _containsValue(other.min) ||
|
|
||||||
_containsValue(other.max) ||
|
|
||||||
other._containsValue(min) ||
|
|
||||||
other._containsValue(max);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) {
|
|
||||||
return other is NumericExtents && min == other.min && max == other.max;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => (min.hashCode + (max.hashCode * 31));
|
|
||||||
|
|
||||||
num get width => max - min;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'Extent($min, $max)';
|
|
||||||
|
|
||||||
static const NumericExtents unbounded =
|
|
||||||
NumericExtents(double.negativeInfinity, double.infinity);
|
|
||||||
static const NumericExtents empty = NumericExtents(0.0, 0.0);
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'numeric_extents.dart' show NumericExtents;
|
|
||||||
import 'scale.dart' show MutableScale;
|
|
||||||
|
|
||||||
/// Scale used to convert numeric domain input units to output range units.
|
|
||||||
///
|
|
||||||
/// The input represents a continuous numeric domain which maps to a given range
|
|
||||||
/// output. This is used to map the domain's values to the available pixel
|
|
||||||
/// range of the chart.
|
|
||||||
abstract class NumericScale extends MutableScale<num> {
|
|
||||||
/// Keeps the scale and translate sane if true (default).
|
|
||||||
///
|
|
||||||
/// Setting this to false disables some pan/zoom protections that prevent you
|
|
||||||
/// from going beyond the data extent.
|
|
||||||
bool get keepViewportWithinData;
|
|
||||||
set keepViewportWithinData(bool keep);
|
|
||||||
|
|
||||||
/// Returns the extent of the actual data (not the viewport max).
|
|
||||||
NumericExtents get dataExtent;
|
|
||||||
|
|
||||||
/// Returns the minimum step size of the actual data.
|
|
||||||
num get minimumDomainStep;
|
|
||||||
|
|
||||||
/// Overrides the domain extent if set, null otherwise.
|
|
||||||
///
|
|
||||||
/// Overrides the extent of the actual data to lie about the range of the
|
|
||||||
/// data so that panning has a start and end point to go between beyond the
|
|
||||||
/// received data. This allows lazy loading of data into the gaps in the
|
|
||||||
/// expanded lied about areas.
|
|
||||||
NumericExtents get domainOverride;
|
|
||||||
set domainOverride(NumericExtents extent);
|
|
||||||
|
|
||||||
/// Returns the domain extent visible in the viewport of the drawArea.
|
|
||||||
NumericExtents get viewportDomain;
|
|
||||||
|
|
||||||
/// Sets the domain extent visible in the viewport of the drawArea.
|
|
||||||
///
|
|
||||||
/// Invalidates the viewportScale & viewportTranslatePx.
|
|
||||||
set viewportDomain(NumericExtents extent);
|
|
||||||
|
|
||||||
/// Returns the viewportScaleFactor needed to present the given domainWindow.
|
|
||||||
double computeViewportScaleFactor(double domainWindow);
|
|
||||||
}
|
|
||||||
@@ -1,584 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show log, log10e, max, min, pow;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../../common/unitconverter/identity_converter.dart'
|
|
||||||
show IdentityConverter;
|
|
||||||
import '../../common/unitconverter/unit_converter.dart' show UnitConverter;
|
|
||||||
import 'axis.dart' show AxisOrientation;
|
|
||||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import 'numeric_extents.dart' show NumericExtents;
|
|
||||||
import 'numeric_scale.dart' show NumericScale;
|
|
||||||
import 'tick.dart' show Tick;
|
|
||||||
import 'tick_formatter.dart' show TickFormatter;
|
|
||||||
import 'tick_provider.dart' show BaseTickProvider, TickHint;
|
|
||||||
|
|
||||||
/// Tick provider that allows you to specify how many ticks to present while
|
|
||||||
/// also choosing tick values that appear "nice" or "rounded" to the user. By
|
|
||||||
/// default it will try to guess an appropriate number of ticks given the size
|
|
||||||
/// of the range available, but the min and max tick counts can be set by
|
|
||||||
/// calling setTickCounts().
|
|
||||||
///
|
|
||||||
/// You can control whether the axis is bound to zero (default) or follows the
|
|
||||||
/// data by calling setZeroBound().
|
|
||||||
///
|
|
||||||
/// This provider will choose "nice" ticks with the following priority order.
|
|
||||||
/// * Ticks do not collide with each other.
|
|
||||||
/// * Alternate rendering is not used to avoid collisions.
|
|
||||||
/// * Provide the least amount of domain range covering all data points (while
|
|
||||||
/// still selecting "nice" ticks values.
|
|
||||||
class NumericTickProvider extends BaseTickProvider<num> {
|
|
||||||
/// Used to determine the automatic tick count calculation.
|
|
||||||
static const MIN_DIPS_BETWEEN_TICKS = 25;
|
|
||||||
|
|
||||||
/// Potential steps available to the baseTen value of the data.
|
|
||||||
static const DEFAULT_STEPS = [
|
|
||||||
0.01,
|
|
||||||
0.02,
|
|
||||||
0.025,
|
|
||||||
0.03,
|
|
||||||
0.04,
|
|
||||||
0.05,
|
|
||||||
0.06,
|
|
||||||
0.07,
|
|
||||||
0.08,
|
|
||||||
0.09,
|
|
||||||
0.1,
|
|
||||||
0.2,
|
|
||||||
0.25,
|
|
||||||
0.3,
|
|
||||||
0.4,
|
|
||||||
0.5,
|
|
||||||
0.6,
|
|
||||||
0.7,
|
|
||||||
0.8,
|
|
||||||
0.9,
|
|
||||||
1.0,
|
|
||||||
2.0,
|
|
||||||
2.50,
|
|
||||||
3.0,
|
|
||||||
4.0,
|
|
||||||
5.0,
|
|
||||||
6.0,
|
|
||||||
7.0,
|
|
||||||
8.0,
|
|
||||||
9.0
|
|
||||||
];
|
|
||||||
|
|
||||||
// Settings
|
|
||||||
|
|
||||||
/// Sets whether the the tick provider should always include a zero tick.
|
|
||||||
///
|
|
||||||
/// If set the data range may be extended to include zero.
|
|
||||||
///
|
|
||||||
/// Note that the zero value in axis units is chosen, which may be different
|
|
||||||
/// than zero value in data units if a data to axis unit converter is set.
|
|
||||||
bool zeroBound = true;
|
|
||||||
|
|
||||||
/// If your data can only be in whole numbers, then set this to true.
|
|
||||||
///
|
|
||||||
/// It should prevent the scale from choosing fractional ticks. For example,
|
|
||||||
/// if you had a office head count, don't generate a tick for 1.5, instead
|
|
||||||
/// jump to 2.
|
|
||||||
///
|
|
||||||
/// Note that the provider will choose whole number ticks in the axis units,
|
|
||||||
/// not data units if a data to axis unit converter is set.
|
|
||||||
bool dataIsInWholeNumbers = true;
|
|
||||||
|
|
||||||
// Desired min and max tick counts are set by [setFixedTickCount] and
|
|
||||||
// [setTickCount]. These are not guaranteed tick counts.
|
|
||||||
int _desiredMaxTickCount;
|
|
||||||
int _desiredMinTickCount;
|
|
||||||
|
|
||||||
/// Allowed steps the tick provider can choose from.
|
|
||||||
var _allowedSteps = DEFAULT_STEPS;
|
|
||||||
|
|
||||||
/// Convert input data units to the desired units on the axis.
|
|
||||||
/// If not set no conversion will take place.
|
|
||||||
///
|
|
||||||
/// Combining this with an appropriate [TickFormatter] would result in axis
|
|
||||||
/// ticks that are in different unit than the actual data units.
|
|
||||||
UnitConverter<num, num> dataToAxisUnitConverter =
|
|
||||||
const IdentityConverter<num>();
|
|
||||||
|
|
||||||
// Tick calculation state
|
|
||||||
num _low;
|
|
||||||
num _high;
|
|
||||||
int _rangeWidth;
|
|
||||||
int _minTickCount;
|
|
||||||
int _maxTickCount;
|
|
||||||
|
|
||||||
// The parameters used in previous tick calculation
|
|
||||||
num _prevLow;
|
|
||||||
num _prevHigh;
|
|
||||||
int _prevRangeWidth;
|
|
||||||
int _prevMinTickCount;
|
|
||||||
int _prevMaxTickCount;
|
|
||||||
bool _prevDataIsInWholeNumbers;
|
|
||||||
|
|
||||||
/// Sets the desired tick count.
|
|
||||||
///
|
|
||||||
/// While the provider will try to satisfy the requirement, it is not
|
|
||||||
/// guaranteed, such as cases where ticks may overlap or are insufficient.
|
|
||||||
///
|
|
||||||
/// [tickCount] the fixed number of major (labeled) ticks to draw for the axis
|
|
||||||
/// Passing null will result in falling back on the automatic tick count
|
|
||||||
/// assignment.
|
|
||||||
void setFixedTickCount(int tickCount) {
|
|
||||||
// Don't allow a single tick, it doesn't make sense. so tickCount > 1
|
|
||||||
_desiredMinTickCount =
|
|
||||||
tickCount != null && tickCount > 1 ? tickCount : null;
|
|
||||||
_desiredMaxTickCount = _desiredMinTickCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the desired min and max tick count when providing ticks.
|
|
||||||
///
|
|
||||||
/// The values are suggested requirements but are not guaranteed to be the
|
|
||||||
/// actual tick count in cases where it is not possible.
|
|
||||||
///
|
|
||||||
/// [maxTickCount] The max tick count must be greater than 1.
|
|
||||||
/// [minTickCount] The min tick count must be greater than 1.
|
|
||||||
void setTickCount(int maxTickCount, int minTickCount) {
|
|
||||||
// Don't allow a single tick, it doesn't make sense. so tickCount > 1
|
|
||||||
if (maxTickCount != null && maxTickCount > 1) {
|
|
||||||
_desiredMaxTickCount = maxTickCount;
|
|
||||||
if (minTickCount != null &&
|
|
||||||
minTickCount > 1 &&
|
|
||||||
minTickCount <= _desiredMaxTickCount) {
|
|
||||||
_desiredMinTickCount = minTickCount;
|
|
||||||
} else {
|
|
||||||
_desiredMinTickCount = 2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_desiredMaxTickCount = null;
|
|
||||||
_desiredMinTickCount = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the allowed step sizes this tick provider can choose from.
|
|
||||||
///
|
|
||||||
/// All ticks will be a power of 10 multiple of the given step sizes.
|
|
||||||
///
|
|
||||||
/// Note that if only very few step sizes are allowed the tick range maybe
|
|
||||||
/// much bigger than the data range.
|
|
||||||
///
|
|
||||||
/// The step sizes setup here apply in axis units, which is different than
|
|
||||||
/// input units if a data to axis unit converter is set.
|
|
||||||
///
|
|
||||||
/// [steps] allowed step sizes in the [1, 10) range.
|
|
||||||
set allowedSteps(List<double> steps) {
|
|
||||||
assert(steps != null && steps.isNotEmpty);
|
|
||||||
steps.sort();
|
|
||||||
|
|
||||||
final stepSet = Set.from(steps);
|
|
||||||
_allowedSteps = List<double>(stepSet.length * 3);
|
|
||||||
int stepIndex = 0;
|
|
||||||
for (double step in stepSet) {
|
|
||||||
assert(1.0 <= step && step < 10.0);
|
|
||||||
_allowedSteps[stepIndex] = _removeRoundingErrors(step / 100);
|
|
||||||
_allowedSteps[stepSet.length + stepIndex] =
|
|
||||||
_removeRoundingErrors(step / 10).toDouble();
|
|
||||||
_allowedSteps[2 * stepSet.length + stepIndex] =
|
|
||||||
_removeRoundingErrors(step);
|
|
||||||
stepIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Tick<num>> _getTicksFromHint({
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required NumericScale scale,
|
|
||||||
@required TickFormatter<num> formatter,
|
|
||||||
@required Map<num, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
@required TickHint<num> tickHint,
|
|
||||||
}) {
|
|
||||||
final stepSize = (tickHint.end - tickHint.start) / (tickHint.tickCount - 1);
|
|
||||||
// Find the first tick that is greater than or equal to the min
|
|
||||||
// viewportDomain.
|
|
||||||
final tickZeroShift = tickHint.start -
|
|
||||||
(stepSize *
|
|
||||||
(tickHint.start >= 0
|
|
||||||
? (tickHint.start / stepSize).floor()
|
|
||||||
: (tickHint.start / stepSize).ceil()));
|
|
||||||
final tickStart =
|
|
||||||
(scale.viewportDomain.min / stepSize).ceil() * stepSize + tickZeroShift;
|
|
||||||
final stepInfo = _TickStepInfo(stepSize.abs(), tickStart);
|
|
||||||
final tickValues = _getTickValues(stepInfo, tickHint.tickCount);
|
|
||||||
|
|
||||||
// Create ticks from domain values.
|
|
||||||
return createTicks(tickValues,
|
|
||||||
context: context,
|
|
||||||
graphicsFactory: graphicsFactory,
|
|
||||||
scale: scale,
|
|
||||||
formatter: formatter,
|
|
||||||
formatterValueCache: formatterValueCache,
|
|
||||||
tickDrawStrategy: tickDrawStrategy,
|
|
||||||
stepSize: stepInfo.stepSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Tick<num>> getTicks({
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required NumericScale scale,
|
|
||||||
@required TickFormatter<num> formatter,
|
|
||||||
@required Map<num, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
@required AxisOrientation orientation,
|
|
||||||
bool viewportExtensionEnabled = false,
|
|
||||||
TickHint<num> tickHint,
|
|
||||||
}) {
|
|
||||||
List<Tick<num>> ticks;
|
|
||||||
|
|
||||||
_rangeWidth = scale.rangeWidth;
|
|
||||||
_updateDomainExtents(scale.viewportDomain);
|
|
||||||
|
|
||||||
// Bypass searching for a tick range since we are getting ticks using
|
|
||||||
// information in [tickHint].
|
|
||||||
if (tickHint != null) {
|
|
||||||
return _getTicksFromHint(
|
|
||||||
context: context,
|
|
||||||
graphicsFactory: graphicsFactory,
|
|
||||||
scale: scale,
|
|
||||||
formatter: formatter,
|
|
||||||
formatterValueCache: formatterValueCache,
|
|
||||||
tickDrawStrategy: tickDrawStrategy,
|
|
||||||
tickHint: tickHint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_hasTickParametersChanged() || ticks == null) {
|
|
||||||
var selectedTicksRange = double.maxFinite;
|
|
||||||
var foundPreferredTicks = false;
|
|
||||||
var viewportDomain = scale.viewportDomain;
|
|
||||||
final axisUnitsHigh = dataToAxisUnitConverter.convert(_high);
|
|
||||||
final axisUnitsLow = dataToAxisUnitConverter.convert(_low);
|
|
||||||
|
|
||||||
_updateTickCounts(axisUnitsHigh, axisUnitsLow);
|
|
||||||
|
|
||||||
// Only create a copy of the scale if [viewportExtensionEnabled].
|
|
||||||
NumericScale mutableScale =
|
|
||||||
viewportExtensionEnabled ? scale.copy() : null;
|
|
||||||
|
|
||||||
// Walk to available tick count from max to min looking for the first one
|
|
||||||
// that gives you the least amount of range used. If a non colliding tick
|
|
||||||
// count is not found use the min tick count to generate ticks.
|
|
||||||
for (int tickCount = _maxTickCount;
|
|
||||||
tickCount >= _minTickCount;
|
|
||||||
tickCount--) {
|
|
||||||
final stepInfo =
|
|
||||||
_getStepsForTickCount(tickCount, axisUnitsHigh, axisUnitsLow);
|
|
||||||
if (stepInfo == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
final firstTick = dataToAxisUnitConverter.invert(stepInfo.tickStart);
|
|
||||||
final lastTick = dataToAxisUnitConverter
|
|
||||||
.invert(stepInfo.tickStart + stepInfo.stepSize * (tickCount - 1));
|
|
||||||
final range = lastTick - firstTick;
|
|
||||||
// Calculate ticks if it is a better range or if preferred ticks have
|
|
||||||
// not been found yet.
|
|
||||||
if (range < selectedTicksRange || !foundPreferredTicks) {
|
|
||||||
final tickValues = _getTickValues(stepInfo, tickCount);
|
|
||||||
|
|
||||||
if (viewportExtensionEnabled) {
|
|
||||||
mutableScale.viewportDomain = NumericExtents(firstTick, lastTick);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ticks from domain values.
|
|
||||||
final preferredTicks = createTicks(tickValues,
|
|
||||||
context: context,
|
|
||||||
graphicsFactory: graphicsFactory,
|
|
||||||
scale: viewportExtensionEnabled ? mutableScale : scale,
|
|
||||||
formatter: formatter,
|
|
||||||
formatterValueCache: formatterValueCache,
|
|
||||||
tickDrawStrategy: tickDrawStrategy,
|
|
||||||
stepSize: stepInfo.stepSize);
|
|
||||||
|
|
||||||
// Request collision check from draw strategy.
|
|
||||||
final collisionReport =
|
|
||||||
tickDrawStrategy.collides(preferredTicks, orientation);
|
|
||||||
|
|
||||||
// Don't choose colliding ticks unless it was our last resort
|
|
||||||
if (collisionReport.ticksCollide && tickCount > _minTickCount) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Only choose alternate ticks if preferred ticks is not found.
|
|
||||||
if (foundPreferredTicks && collisionReport.alternateTicksUsed) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ticks = collisionReport.alternateTicksUsed
|
|
||||||
? collisionReport.ticks
|
|
||||||
: preferredTicks;
|
|
||||||
foundPreferredTicks = !collisionReport.alternateTicksUsed;
|
|
||||||
selectedTicksRange = range;
|
|
||||||
// If viewport extended, save the viewport used.
|
|
||||||
viewportDomain = mutableScale?.viewportDomain ?? scale.viewportDomain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_setPreviousTickCalculationParameters();
|
|
||||||
// If [viewportExtensionEnabled] and has changed, then set the scale's
|
|
||||||
// viewport to what was used to generate ticks. By only setting viewport
|
|
||||||
// when it has changed, we do not trigger the flag to recalculate scale.
|
|
||||||
if (viewportExtensionEnabled && scale.viewportDomain != viewportDomain) {
|
|
||||||
scale.viewportDomain = viewportDomain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ticks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks whether the parameters that are used in determining the right set
|
|
||||||
/// of ticks changed from the last time we calculated ticks. If not we should
|
|
||||||
/// be able to use the cached ticks.
|
|
||||||
bool _hasTickParametersChanged() {
|
|
||||||
return _low != _prevLow ||
|
|
||||||
_high != _prevHigh ||
|
|
||||||
_rangeWidth != _prevRangeWidth ||
|
|
||||||
_minTickCount != _prevMinTickCount ||
|
|
||||||
_maxTickCount != _prevMaxTickCount ||
|
|
||||||
dataIsInWholeNumbers != _prevDataIsInWholeNumbers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save the last set of parameters used while determining ticks.
|
|
||||||
void _setPreviousTickCalculationParameters() {
|
|
||||||
_prevLow = _low;
|
|
||||||
_prevHigh = _high;
|
|
||||||
_prevRangeWidth = _rangeWidth;
|
|
||||||
_prevMinTickCount = _minTickCount;
|
|
||||||
_prevMaxTickCount = _maxTickCount;
|
|
||||||
_prevDataIsInWholeNumbers = dataIsInWholeNumbers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the domain extents that this provider will cover based on the
|
|
||||||
/// axis extents passed in and the settings in the numeric tick provider.
|
|
||||||
/// Stores the domain extents in [_low] and [_high].
|
|
||||||
void _updateDomainExtents(NumericExtents axisExtents) {
|
|
||||||
_low = axisExtents.min;
|
|
||||||
_high = axisExtents.max;
|
|
||||||
|
|
||||||
// Correct the extents for zero bound
|
|
||||||
if (zeroBound) {
|
|
||||||
_low = _low > 0.0 ? 0.0 : _low;
|
|
||||||
_high = _high < 0.0 ? 0.0 : _high;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Correct cases where high and low equal to give the tick provider an
|
|
||||||
// actual range to go off of when picking ticks.
|
|
||||||
if (_high == _low) {
|
|
||||||
if (_high == 0.0) {
|
|
||||||
// Corner case: the only values we've seen are zero, so lets just say
|
|
||||||
// the high is 1 and leave the low at zero.
|
|
||||||
_high = 1.0;
|
|
||||||
} else {
|
|
||||||
// The values are all the same, so assume a range of -5% to +5% from the
|
|
||||||
// single value.
|
|
||||||
if (_high > 0.0) {
|
|
||||||
_high = _high * 1.05;
|
|
||||||
_low = _low * 0.95;
|
|
||||||
} else {
|
|
||||||
// (high == low) < 0
|
|
||||||
_high = _high * 0.95;
|
|
||||||
_low = _low * 1.05;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given [tickCount] and the domain range, finds the smallest tick increment,
|
|
||||||
/// chosen from power of 10 multiples of allowed steps, that covers the whole
|
|
||||||
/// data range.
|
|
||||||
_TickStepInfo _getStepsForTickCount(int tickCount, num high, num low) {
|
|
||||||
// A region is the space between ticks.
|
|
||||||
final regionCount = tickCount - 1;
|
|
||||||
|
|
||||||
// If the range contains zero, ensure that zero is a tick.
|
|
||||||
if (high >= 0 && low <= 0) {
|
|
||||||
// determine the ratio of regions that are above the zero axis.
|
|
||||||
final posRegionRatio = (high > 0 ? min(1.0, high / (high - low)) : 0.0);
|
|
||||||
var positiveRegionCount = (regionCount * posRegionRatio).ceil();
|
|
||||||
var negativeRegionCount = regionCount - positiveRegionCount;
|
|
||||||
// Ensure that negative regions are not excluded, unless there are no
|
|
||||||
// regions to spare.
|
|
||||||
if (negativeRegionCount == 0 && low < 0 && regionCount > 1) {
|
|
||||||
positiveRegionCount--;
|
|
||||||
negativeRegionCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have positive and negative values, ensure that we have ticks in
|
|
||||||
// both regions.
|
|
||||||
//
|
|
||||||
// This should not happen unless the axis is manually configured with a
|
|
||||||
// tick count. [_updateTickCounts] should ensure that we have do not try
|
|
||||||
// to generate fewer than three.
|
|
||||||
assert(
|
|
||||||
!(low < 0 &&
|
|
||||||
high > 0 &&
|
|
||||||
(negativeRegionCount == 0 || positiveRegionCount == 0)),
|
|
||||||
'Numeric tick provider cannot generate $tickCount '
|
|
||||||
'ticks when the axis range contains both positive and negative '
|
|
||||||
'values. A minimum of three ticks are required to include zero.');
|
|
||||||
|
|
||||||
// Determine the "favored" axis direction (the one which will control the
|
|
||||||
// ticks based on having a greater value / regions).
|
|
||||||
//
|
|
||||||
// Example: 13 / 3 (4.33 per tick) vs -5 / 1 (5 per tick)
|
|
||||||
// making -5 the favored number. A step size that includes this number
|
|
||||||
// ensures the other is also includes in the opposite direction.
|
|
||||||
final favorPositive = (high > 0 ? high / positiveRegionCount : 0).abs() >
|
|
||||||
(low < 0 ? low / negativeRegionCount : 0).abs();
|
|
||||||
final favoredNum = (favorPositive ? high : low).abs();
|
|
||||||
final favoredRegionCount =
|
|
||||||
favorPositive ? positiveRegionCount : negativeRegionCount;
|
|
||||||
final favoredTensBase = (_getEnclosingPowerOfTen(favoredNum)).abs();
|
|
||||||
|
|
||||||
// Check each step size and see if it would contain the "favored" value
|
|
||||||
for (double step in _allowedSteps) {
|
|
||||||
final tmpStepSize = _removeRoundingErrors(step * favoredTensBase);
|
|
||||||
|
|
||||||
// If prefer whole number, then don't allow a step that isn't one.
|
|
||||||
if (dataIsInWholeNumbers && (tmpStepSize).round() != tmpStepSize) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Skip steps that format to the same string.
|
|
||||||
// But wait until the last step to prevent the cost of the formatter.
|
|
||||||
// Potentially store the formatted strings in TickStepInfo?
|
|
||||||
if (tmpStepSize * favoredRegionCount >= favoredNum) {
|
|
||||||
double stepStart = negativeRegionCount > 0
|
|
||||||
? (-1 * tmpStepSize * negativeRegionCount)
|
|
||||||
: 0.0;
|
|
||||||
return _TickStepInfo(tmpStepSize, stepStart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Find the range base to calculate step sizes.
|
|
||||||
final diffTensBase = _getEnclosingPowerOfTen(high - low);
|
|
||||||
// Walk the step sizes calculating a starting point and seeing if the high
|
|
||||||
// end is included in the range given that step size.
|
|
||||||
for (double step in _allowedSteps) {
|
|
||||||
final tmpStepSize = _removeRoundingErrors(step * diffTensBase);
|
|
||||||
|
|
||||||
// If prefer whole number, then don't allow a step that isn't one.
|
|
||||||
if (dataIsInWholeNumbers && (tmpStepSize).round() != tmpStepSize) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Skip steps that format to the same string.
|
|
||||||
// But wait until the last step to prevent the cost of the formatter.
|
|
||||||
double tmpStepStart = _getStepLessThan(low, tmpStepSize);
|
|
||||||
if (tmpStepStart + (tmpStepSize * regionCount) >= high) {
|
|
||||||
return _TickStepInfo(tmpStepSize, tmpStepStart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _TickStepInfo(1.0, low.floorToDouble());
|
|
||||||
}
|
|
||||||
|
|
||||||
List<double> _getTickValues(_TickStepInfo steps, int tickCount) {
|
|
||||||
final tickValues = List<double>(tickCount);
|
|
||||||
// We have our size and start, assign all the tick values to the given array.
|
|
||||||
for (int i = 0; i < tickCount; i++) {
|
|
||||||
tickValues[i] = dataToAxisUnitConverter.invert(
|
|
||||||
_removeRoundingErrors(steps.tickStart + (i * steps.stepSize)));
|
|
||||||
}
|
|
||||||
return tickValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Given the axisDimensions update the tick counts given they are not fixed.
|
|
||||||
void _updateTickCounts(num high, num low) {
|
|
||||||
int tmpMaxNumMajorTicks;
|
|
||||||
int tmpMinNumMajorTicks;
|
|
||||||
|
|
||||||
// If the domain range contains both positive and negative values, then we
|
|
||||||
// need a minimum of three ticks to include zero as a tick. Otherwise, we
|
|
||||||
// only need an upper and lower tick.
|
|
||||||
final absoluteMinTicks = (low < 0 && 0 < high) ? 3 : 2;
|
|
||||||
|
|
||||||
// If there is a desired tick range use it, if not calculate one.
|
|
||||||
if (_desiredMaxTickCount != null) {
|
|
||||||
tmpMinNumMajorTicks = max(_desiredMinTickCount, absoluteMinTicks);
|
|
||||||
tmpMaxNumMajorTicks = max(_desiredMaxTickCount, tmpMinNumMajorTicks);
|
|
||||||
} else {
|
|
||||||
double minPixelsPerTick = MIN_DIPS_BETWEEN_TICKS.toDouble();
|
|
||||||
tmpMinNumMajorTicks = absoluteMinTicks;
|
|
||||||
tmpMaxNumMajorTicks =
|
|
||||||
max(absoluteMinTicks, (_rangeWidth / minPixelsPerTick).floor());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't blow away the previous array if it hasn't changed.
|
|
||||||
if (tmpMaxNumMajorTicks != _maxTickCount ||
|
|
||||||
tmpMinNumMajorTicks != _minTickCount) {
|
|
||||||
_maxTickCount = tmpMaxNumMajorTicks;
|
|
||||||
_minTickCount = tmpMinNumMajorTicks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the power of 10 which contains the [number].
|
|
||||||
///
|
|
||||||
/// If [number] is 0 returns 1.
|
|
||||||
/// Examples:
|
|
||||||
/// [number] of 63 returns 100
|
|
||||||
/// [number] of -63 returns -100
|
|
||||||
/// [number] of 0.63 returns 1
|
|
||||||
static double _getEnclosingPowerOfTen(num number) {
|
|
||||||
if (number == 0) {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pow(10, (log10e * log(number.abs())).ceil()) *
|
|
||||||
(number < 0.0 ? -1.0 : 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the step numerically less than the number by step increments.
|
|
||||||
static double _getStepLessThan(double number, double stepSize) {
|
|
||||||
if (number == 0.0 || stepSize == 0.0) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
return (stepSize > 0.0
|
|
||||||
? (number / stepSize).floor()
|
|
||||||
: (number / stepSize).ceil()) *
|
|
||||||
stepSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to slice off very small floating point rounding effects for the
|
|
||||||
/// given number.
|
|
||||||
///
|
|
||||||
/// @param number the number to round.
|
|
||||||
/// @return the rounded number.
|
|
||||||
static double _removeRoundingErrors(double number) {
|
|
||||||
// sufficiently large multiplier to handle generating ticks on the order
|
|
||||||
// of 10^-9.
|
|
||||||
const multiplier = 1.0e9;
|
|
||||||
|
|
||||||
return number > 100.0
|
|
||||||
? number.roundToDouble()
|
|
||||||
: (number * multiplier).roundToDouble() / multiplier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TickStepInfo {
|
|
||||||
double stepSize;
|
|
||||||
double tickStart;
|
|
||||||
|
|
||||||
_TickStepInfo(this.stepSize, this.tickStart);
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:collection' show HashSet;
|
|
||||||
import 'scale.dart' show Extents;
|
|
||||||
|
|
||||||
/// A range of ordinals.
|
|
||||||
class OrdinalExtents extends Extents<String> {
|
|
||||||
final List<String> _range;
|
|
||||||
|
|
||||||
/// The extents representing the ordinal values in [range].
|
|
||||||
///
|
|
||||||
/// The elements of [range] must all be unique.
|
|
||||||
///
|
|
||||||
/// [D] is the domain class type for the elements in the extents.
|
|
||||||
OrdinalExtents(List<String> range) : _range = range {
|
|
||||||
// This asserts that all elements in [range] are unique.
|
|
||||||
final uniqueValueCount = HashSet.from(_range).length;
|
|
||||||
assert(uniqueValueCount == range.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory OrdinalExtents.all(List<String> range) => OrdinalExtents(range);
|
|
||||||
|
|
||||||
bool get isEmpty => _range.isEmpty;
|
|
||||||
|
|
||||||
/// The number of values inside this extent.
|
|
||||||
int get length => _range.length;
|
|
||||||
|
|
||||||
String operator [](int index) => _range[index];
|
|
||||||
|
|
||||||
int indexOf(String value) => _range.indexOf(value);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'ordinal_scale_domain_info.dart' show OrdinalScaleDomainInfo;
|
|
||||||
import 'scale.dart' show MutableScale;
|
|
||||||
|
|
||||||
abstract class OrdinalScale extends MutableScale<String> {
|
|
||||||
/// The current domain collection with all added unique values.
|
|
||||||
OrdinalScaleDomainInfo get domain;
|
|
||||||
|
|
||||||
/// Sets the viewport of the scale based on the number of data points to show
|
|
||||||
/// and the starting domain value.
|
|
||||||
///
|
|
||||||
/// [viewportDataSize] How many ordinal domain values to show in the viewport.
|
|
||||||
/// [startingDomain] The starting domain value of the viewport. Note that if
|
|
||||||
/// the starting domain is in terms of position less than [domainValuesToShow]
|
|
||||||
/// from the last domain value the viewport will be fixed to the last value
|
|
||||||
/// and not guaranteed that this domain value is the first in the viewport.
|
|
||||||
void setViewport(int viewportDataSize, String startingDomain);
|
|
||||||
|
|
||||||
/// The number of full ordinal steps that fit in the viewport.
|
|
||||||
int get viewportDataSize;
|
|
||||||
|
|
||||||
/// The first fully visible ordinal step within the viewport.
|
|
||||||
///
|
|
||||||
/// Null if no domains exist.
|
|
||||||
String get viewportStartingDomain;
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:collection' show HashMap;
|
|
||||||
import 'ordinal_extents.dart' show OrdinalExtents;
|
|
||||||
|
|
||||||
/// A domain processor for [OrdinalScale].
|
|
||||||
///
|
|
||||||
/// [D] domain class type of the values being tracked.
|
|
||||||
///
|
|
||||||
/// Unique domain values are kept, so duplicates will not increase the extent.
|
|
||||||
class OrdinalScaleDomainInfo {
|
|
||||||
int _index = 0;
|
|
||||||
|
|
||||||
/// A map of domain value and the order it was added.
|
|
||||||
final _domainsToOrder = HashMap<String, int>();
|
|
||||||
|
|
||||||
/// A list of domain values kept to support [getDomainAtIndex].
|
|
||||||
final _domainList = <String>[];
|
|
||||||
|
|
||||||
OrdinalScaleDomainInfo();
|
|
||||||
|
|
||||||
OrdinalScaleDomainInfo copy() {
|
|
||||||
return OrdinalScaleDomainInfo()
|
|
||||||
.._domainsToOrder.addAll(_domainsToOrder)
|
|
||||||
.._index = _index
|
|
||||||
.._domainList.addAll(_domainList);
|
|
||||||
}
|
|
||||||
|
|
||||||
void add(String domain) {
|
|
||||||
if (!_domainsToOrder.containsKey(domain)) {
|
|
||||||
_domainsToOrder[domain] = _index;
|
|
||||||
_index += 1;
|
|
||||||
_domainList.add(domain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int indexOf(String domain) => _domainsToOrder[domain];
|
|
||||||
|
|
||||||
String getDomainAtIndex(int index) {
|
|
||||||
assert(index >= 0);
|
|
||||||
assert(index < _index);
|
|
||||||
return _domainList[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> get domains => _domainList;
|
|
||||||
|
|
||||||
String get first => _domainList.isEmpty ? null : _domainList.first;
|
|
||||||
|
|
||||||
String get last => _domainList.isEmpty ? null : _domainList.last;
|
|
||||||
|
|
||||||
bool get isEmpty => (_index == 0);
|
|
||||||
bool get isNotEmpty => !isEmpty;
|
|
||||||
|
|
||||||
OrdinalExtents get extent => OrdinalExtents.all(_domainList);
|
|
||||||
|
|
||||||
int get size => _index;
|
|
||||||
|
|
||||||
/// Clears all domain values.
|
|
||||||
void clear() {
|
|
||||||
_domainsToOrder.clear();
|
|
||||||
_domainList.clear();
|
|
||||||
_index = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../common/chart_context.dart' show ChartContext;
|
|
||||||
import 'axis.dart' show AxisOrientation;
|
|
||||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import 'ordinal_scale.dart' show OrdinalScale;
|
|
||||||
import 'tick.dart' show Tick;
|
|
||||||
import 'tick_formatter.dart' show TickFormatter;
|
|
||||||
import 'tick_provider.dart' show BaseTickProvider, TickHint;
|
|
||||||
|
|
||||||
/// A strategy for selecting ticks to draw given ordinal domain values.
|
|
||||||
class OrdinalTickProvider extends BaseTickProvider<String> {
|
|
||||||
const OrdinalTickProvider();
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Tick<String>> getTicks({
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required List<String> domainValues,
|
|
||||||
@required OrdinalScale scale,
|
|
||||||
@required TickFormatter formatter,
|
|
||||||
@required Map<String, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
@required AxisOrientation orientation,
|
|
||||||
bool viewportExtensionEnabled = false,
|
|
||||||
TickHint<String> tickHint,
|
|
||||||
}) {
|
|
||||||
return createTicks(scale.domain.domains,
|
|
||||||
context: context,
|
|
||||||
graphicsFactory: graphicsFactory,
|
|
||||||
scale: scale,
|
|
||||||
formatter: formatter,
|
|
||||||
formatterValueCache: formatterValueCache,
|
|
||||||
tickDrawStrategy: tickDrawStrategy);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) => other is OrdinalTickProvider;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => 31;
|
|
||||||
}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' as math show max, min;
|
|
||||||
|
|
||||||
/// Scale used to convert data input domain units to output range units.
|
|
||||||
///
|
|
||||||
/// This is the immutable portion of the Scale definition. Used for converting
|
|
||||||
/// data from the dataset in domain units to an output in range units (likely
|
|
||||||
/// pixel range of the area to draw on).
|
|
||||||
///
|
|
||||||
/// <p>The Scale/MutableScale split is to show the intention of what you can or
|
|
||||||
/// should be doing with the scale during different stages of chart draw
|
|
||||||
/// process.
|
|
||||||
///
|
|
||||||
/// [D] is the domain class type for the values passed in.
|
|
||||||
abstract class Scale<D> {
|
|
||||||
/// Applies the scale function to the [domainValue].
|
|
||||||
///
|
|
||||||
/// Returns the pixel location for the given [domainValue] or null if the
|
|
||||||
/// domainValue could not be found/translated by this scale.
|
|
||||||
/// Non-numeric scales should be the only ones that can return null.
|
|
||||||
num operator [](D domainValue);
|
|
||||||
|
|
||||||
/// Reverse application of the scale.
|
|
||||||
D reverse(double pixelLocation);
|
|
||||||
|
|
||||||
/// Tests a [domainValue] to see if the scale can translate it.
|
|
||||||
///
|
|
||||||
/// Returns true if the scale can translate the given domainValue.
|
|
||||||
/// (Ex: linear scales can translate any number, but ordinal scales can only
|
|
||||||
/// translate values previously passed in.)
|
|
||||||
bool canTranslate(D domainValue);
|
|
||||||
|
|
||||||
/// Returns the previously set output range for the scale function.
|
|
||||||
ScaleOutputExtent get range;
|
|
||||||
|
|
||||||
/// Returns the absolute width between the max and min range values.
|
|
||||||
int get rangeWidth;
|
|
||||||
|
|
||||||
/// Returns the configuration used to determine the rangeBand.
|
|
||||||
///
|
|
||||||
/// This is most often used to define the bar group width.
|
|
||||||
RangeBandConfig get rangeBandConfig;
|
|
||||||
|
|
||||||
/// Returns the rangeBand width in pixels.
|
|
||||||
///
|
|
||||||
/// The rangeBand is determined using the RangeBandConfig potentially with the
|
|
||||||
/// measured step size. This value is used as the bar group width. If
|
|
||||||
/// StepSizeConfig is set to auto detect, then you must wait until after
|
|
||||||
/// the chart's onPostLayout phase before you'll get a valid number.
|
|
||||||
double get rangeBand;
|
|
||||||
|
|
||||||
/// Returns the stepSize width in pixels.
|
|
||||||
///
|
|
||||||
/// The step size is determined using the [StepSizeConfig].
|
|
||||||
double get stepSize;
|
|
||||||
|
|
||||||
/// Returns the stepSize domain value.
|
|
||||||
double get domainStepSize;
|
|
||||||
|
|
||||||
/// Tests whether the given [domainValue] is within the axis' range.
|
|
||||||
///
|
|
||||||
/// Returns < 0 if the [domainValue] would plot before the viewport, 0 if it
|
|
||||||
/// would plot within the viewport and > 0 if it would plot beyond the
|
|
||||||
/// viewport of the axis.
|
|
||||||
int compareDomainValueToViewport(D domainValue);
|
|
||||||
|
|
||||||
/// Returns true if the given [rangeValue] point is within the output range.
|
|
||||||
///
|
|
||||||
/// Not to be confused with the start and end of the domain.
|
|
||||||
bool isRangeValueWithinViewport(double rangeValue);
|
|
||||||
|
|
||||||
/// Returns the current viewport scale.
|
|
||||||
///
|
|
||||||
/// A scale of 1.0 would map the data directly to the output range, while a
|
|
||||||
/// value of 2.0 would map the data to an output of double the range so you
|
|
||||||
/// only see half the data in the viewport. This is the equivalent to
|
|
||||||
/// zooming. Its value is likely >= 1.0.
|
|
||||||
double get viewportScalingFactor;
|
|
||||||
|
|
||||||
/// Returns the current pixel viewport offset
|
|
||||||
///
|
|
||||||
/// The translate is used by the scale function when it applies the scale.
|
|
||||||
/// This is the equivalent to panning. Its value is likely <= 0 to pan the
|
|
||||||
/// data to the left.
|
|
||||||
double get viewportTranslatePx;
|
|
||||||
|
|
||||||
/// Returns a mutable copy of the scale.
|
|
||||||
///
|
|
||||||
/// Mutating the returned scale will not effect the original one.
|
|
||||||
MutableScale<D> copy();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mutable extension of the [Scale] definition.
|
|
||||||
///
|
|
||||||
/// Used for converting data from the dataset to some range (likely pixel range)
|
|
||||||
/// of the area to draw on.
|
|
||||||
///
|
|
||||||
/// [D] the domain class type for the values passed in.
|
|
||||||
abstract class MutableScale<D> extends Scale<D> {
|
|
||||||
/// Reset the domain for this [Scale].
|
|
||||||
void resetDomain();
|
|
||||||
|
|
||||||
/// Reset the viewport settings for this [Scale].
|
|
||||||
void resetViewportSettings();
|
|
||||||
|
|
||||||
/// Add [domainValue] to this [Scale]'s domain.
|
|
||||||
///
|
|
||||||
/// Domains should be added in order to allow proper stepSize detection.
|
|
||||||
/// [domainValue] is the data value to add to the scale used to update the
|
|
||||||
/// domain extent.
|
|
||||||
void addDomain(D domainValue);
|
|
||||||
|
|
||||||
/// Sets the output range to use for the scale's conversion.
|
|
||||||
///
|
|
||||||
/// The range start is mapped to the domain's min and the range end is
|
|
||||||
/// mapped to the domain's max for the conversion using the domain nicing
|
|
||||||
/// function.
|
|
||||||
///
|
|
||||||
/// [extent] is the extent of the range which will likely be the pixel
|
|
||||||
/// range of the drawing area to convert to.
|
|
||||||
set range(ScaleOutputExtent extent);
|
|
||||||
|
|
||||||
/// Configures the zoom and translate.
|
|
||||||
///
|
|
||||||
/// [viewportScale] is the zoom factor to use, likely >= 1.0 where 1.0 maps
|
|
||||||
/// the complete data extents to the output range, and 2.0 only maps half the
|
|
||||||
/// data to the output range.
|
|
||||||
///
|
|
||||||
/// [viewportTranslatePx] is the translate/pan to use in pixel units,
|
|
||||||
/// likely <= 0 which shifts the start of the data before the edge of the
|
|
||||||
/// chart giving us a pan.
|
|
||||||
void setViewportSettings(double viewportScale, double viewportTranslatePx);
|
|
||||||
|
|
||||||
/// Sets the configuration used to determine the rangeBand (bar group width).
|
|
||||||
set rangeBandConfig(RangeBandConfig barGroupWidthConfig);
|
|
||||||
|
|
||||||
/// Sets the method for determining the step size.
|
|
||||||
///
|
|
||||||
/// This is the domain space between data points.
|
|
||||||
StepSizeConfig get stepSizeConfig;
|
|
||||||
set stepSizeConfig(StepSizeConfig config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tuple of the output for a scale in pixels from [start] to [end] inclusive.
|
|
||||||
///
|
|
||||||
/// It is different from [Extent] because it focuses on start and end and not
|
|
||||||
/// min and max, meaning that start could be greater or less than end.
|
|
||||||
class ScaleOutputExtent {
|
|
||||||
final int start;
|
|
||||||
final int end;
|
|
||||||
|
|
||||||
const ScaleOutputExtent(this.start, this.end);
|
|
||||||
|
|
||||||
int get min => math.min(start, end);
|
|
||||||
int get max => math.max(start, end);
|
|
||||||
|
|
||||||
bool containsValue(double value) => value >= min && value <= max;
|
|
||||||
|
|
||||||
/// Returns the difference between the extents.
|
|
||||||
///
|
|
||||||
/// If the [end] is less than the [start] (think vertical measure axis), then
|
|
||||||
/// this will correctly return a negative value.
|
|
||||||
int get diff => end - start;
|
|
||||||
|
|
||||||
/// Returns the width of the extent.
|
|
||||||
int get width => diff.abs();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) =>
|
|
||||||
other is ScaleOutputExtent && start == other.start && end == other.end;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => start.hashCode + (end.hashCode * 31);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => "ScaleOutputRange($start, $end)";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type of RangeBand used to determine the rangeBand size units.
|
|
||||||
enum RangeBandType {
|
|
||||||
/// No rangeBand (not suitable for bars or step line charts).
|
|
||||||
none,
|
|
||||||
|
|
||||||
/// Size is specified in pixel units.
|
|
||||||
fixedPixel,
|
|
||||||
|
|
||||||
/// Size is specified domain scale units.
|
|
||||||
fixedDomain,
|
|
||||||
|
|
||||||
/// Size is a percentage of the minimum step size between points.
|
|
||||||
fixedPercentOfStep,
|
|
||||||
|
|
||||||
/// Size is a style pack assigned percentage of the minimum step size between
|
|
||||||
/// points.
|
|
||||||
styleAssignedPercentOfStep,
|
|
||||||
|
|
||||||
/// Size is subtracted from the minimum step size between points in pixel
|
|
||||||
/// units.
|
|
||||||
fixedPixelSpaceFromStep,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Defines the method for calculating the rangeBand of the Scale.
|
|
||||||
///
|
|
||||||
/// The rangeBand is used to determine the width of a group of bars. The term
|
|
||||||
/// rangeBand comes from the d3 JavaScript library which the JS library uses
|
|
||||||
/// internally.
|
|
||||||
///
|
|
||||||
/// <p>RangeBandConfig is immutable, See factory methods for creating one.
|
|
||||||
class RangeBandConfig {
|
|
||||||
final RangeBandType type;
|
|
||||||
|
|
||||||
/// The width of the band in units specified by the bandType.
|
|
||||||
final double size;
|
|
||||||
|
|
||||||
/// Creates a rangeBand definition of zero, no rangeBand.
|
|
||||||
const RangeBandConfig.none()
|
|
||||||
: type = RangeBandType.none,
|
|
||||||
size = 0.0;
|
|
||||||
|
|
||||||
/// Creates a fixed rangeBand definition in pixel width.
|
|
||||||
///
|
|
||||||
/// Used to determine a bar width or a step width in the line renderer.
|
|
||||||
const RangeBandConfig.fixedPixel(double pixels)
|
|
||||||
: type = RangeBandType.fixedPixel,
|
|
||||||
size = pixels;
|
|
||||||
|
|
||||||
/// Creates a fixed rangeBand definition in domain unit width.
|
|
||||||
///
|
|
||||||
/// Used to determine a bar width or a step width in the line renderer.
|
|
||||||
const RangeBandConfig.fixedDomain(double domainSize)
|
|
||||||
: type = RangeBandType.fixedDomain,
|
|
||||||
size = domainSize;
|
|
||||||
|
|
||||||
/// Creates a config that defines the rangeBand as equal to the stepSize.
|
|
||||||
const RangeBandConfig.stepChartBand()
|
|
||||||
: type = RangeBandType.fixedPercentOfStep,
|
|
||||||
size = 1.0;
|
|
||||||
|
|
||||||
/// Creates a config that defines the rangeBand as percentage of the stepSize.
|
|
||||||
///
|
|
||||||
/// [percentOfStepWidth] is the percentage of the step from 0.0 - 1.0.
|
|
||||||
RangeBandConfig.percentOfStep(double percentOfStepWidth)
|
|
||||||
: type = RangeBandType.fixedPercentOfStep,
|
|
||||||
size = percentOfStepWidth {
|
|
||||||
assert(percentOfStepWidth >= 0 && percentOfStepWidth <= 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a config that assigns the rangeBand according to the stylepack.
|
|
||||||
///
|
|
||||||
/// <p>Note: renderers can detect this setting and update the percent based on
|
|
||||||
/// the number of series in their preprocess.
|
|
||||||
RangeBandConfig.styleAssignedPercent([int seriesCount = 1])
|
|
||||||
: type = RangeBandType.styleAssignedPercentOfStep,
|
|
||||||
// TODO: retrieve value from the stylepack once available.
|
|
||||||
size = 0.65;
|
|
||||||
|
|
||||||
/// Creates a config that defines the rangeBand as the stepSize - pixels.
|
|
||||||
///
|
|
||||||
/// Where fixedPixels() gave you a constant rangBand in pixels, this will give
|
|
||||||
/// you a constant space between rangeBands in pixels.
|
|
||||||
const RangeBandConfig.fixedPixelSpaceBetweenStep(double pixels)
|
|
||||||
: type = RangeBandType.fixedPixelSpaceFromStep,
|
|
||||||
size = pixels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type of step size calculation to use.
|
|
||||||
enum StepSizeType { autoDetect, fixedDomain, fixedPixels }
|
|
||||||
|
|
||||||
/// Defines the method for calculating the stepSize between points.
|
|
||||||
///
|
|
||||||
/// Typically auto will work fine in most cases, but if your data is
|
|
||||||
/// irregular or you only have one data point, then you may want to override the
|
|
||||||
/// stepSize detection specifying the exact expected stepSize.
|
|
||||||
class StepSizeConfig {
|
|
||||||
final StepSizeType type;
|
|
||||||
final double size;
|
|
||||||
|
|
||||||
/// Creates a StepSizeConfig that calculates step size based on incoming data.
|
|
||||||
///
|
|
||||||
/// The stepSize is determined is calculated by detecting the smallest
|
|
||||||
/// distance between two adjacent data points. This may not be suitable if
|
|
||||||
/// you have irregular data or just a single data point.
|
|
||||||
const StepSizeConfig.auto()
|
|
||||||
: type = StepSizeType.autoDetect,
|
|
||||||
size = 0.0;
|
|
||||||
|
|
||||||
/// Creates a StepSizeConfig specifying the exact step size in pixel units.
|
|
||||||
const StepSizeConfig.fixedPixels(double pixels)
|
|
||||||
: type = StepSizeType.fixedPixels,
|
|
||||||
size = pixels;
|
|
||||||
|
|
||||||
/// Creates a StepSizeConfig specifying the exact step size in domain units.
|
|
||||||
const StepSizeConfig.fixedDomain(double domainSize)
|
|
||||||
: type = StepSizeType.fixedDomain,
|
|
||||||
size = domainSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make other extent subclasses plural.
|
|
||||||
abstract class Extents<D> {}
|
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show min, max;
|
|
||||||
|
|
||||||
import 'ordinal_scale.dart' show OrdinalScale;
|
|
||||||
import 'ordinal_scale_domain_info.dart' show OrdinalScaleDomainInfo;
|
|
||||||
import 'scale.dart'
|
|
||||||
show
|
|
||||||
RangeBandConfig,
|
|
||||||
RangeBandType,
|
|
||||||
StepSizeConfig,
|
|
||||||
StepSizeType,
|
|
||||||
ScaleOutputExtent;
|
|
||||||
|
|
||||||
/// Scale that converts ordinal values of type [D] to a given range output.
|
|
||||||
///
|
|
||||||
/// A `SimpleOrdinalScale` is used to map values from its domain to the
|
|
||||||
/// available pixel range of the chart. Typically used for bar charts where the
|
|
||||||
/// width of the bar is [rangeBand] and the position of the bar is retrieved
|
|
||||||
/// by [[]].
|
|
||||||
class SimpleOrdinalScale implements OrdinalScale {
|
|
||||||
final _stepSizeConfig = StepSizeConfig.auto();
|
|
||||||
OrdinalScaleDomainInfo _domain;
|
|
||||||
ScaleOutputExtent _range = ScaleOutputExtent(0, 1);
|
|
||||||
double _viewportScale = 1.0;
|
|
||||||
double _viewportTranslatePx = 0.0;
|
|
||||||
RangeBandConfig _rangeBandConfig = RangeBandConfig.styleAssignedPercent();
|
|
||||||
|
|
||||||
bool _scaleChanged = true;
|
|
||||||
double _cachedStepSizePixels;
|
|
||||||
double _cachedRangeBandShift;
|
|
||||||
double _cachedRangeBandSize;
|
|
||||||
|
|
||||||
int _viewportDataSize;
|
|
||||||
String _viewportStartingDomain;
|
|
||||||
|
|
||||||
SimpleOrdinalScale() : _domain = OrdinalScaleDomainInfo();
|
|
||||||
|
|
||||||
SimpleOrdinalScale._copy(SimpleOrdinalScale other)
|
|
||||||
: _domain = other._domain.copy(),
|
|
||||||
_range = ScaleOutputExtent(other._range.start, other._range.end),
|
|
||||||
_viewportScale = other._viewportScale,
|
|
||||||
_viewportTranslatePx = other._viewportTranslatePx,
|
|
||||||
_rangeBandConfig = other._rangeBandConfig;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get rangeBand {
|
|
||||||
if (_scaleChanged) {
|
|
||||||
_updateScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _cachedRangeBandSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get stepSize {
|
|
||||||
if (_scaleChanged) {
|
|
||||||
_updateScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _cachedStepSizePixels;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get domainStepSize => 1.0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set rangeBandConfig(RangeBandConfig barGroupWidthConfig) {
|
|
||||||
if (barGroupWidthConfig == null) {
|
|
||||||
throw ArgumentError.notNull('RangeBandConfig must not be null.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (barGroupWidthConfig.type == RangeBandType.fixedDomain ||
|
|
||||||
barGroupWidthConfig.type == RangeBandType.none) {
|
|
||||||
throw ArgumentError(
|
|
||||||
'barGroupWidthConfig must not be NONE or FIXED_DOMAIN');
|
|
||||||
}
|
|
||||||
|
|
||||||
_rangeBandConfig = barGroupWidthConfig;
|
|
||||||
_scaleChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
RangeBandConfig get rangeBandConfig => _rangeBandConfig;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set stepSizeConfig(StepSizeConfig config) {
|
|
||||||
if (config != null && config.type != StepSizeType.autoDetect) {
|
|
||||||
throw ArgumentError(
|
|
||||||
'Ordinal scales only support StepSizeConfig of type Auto');
|
|
||||||
}
|
|
||||||
// Nothing is set because only auto is supported.
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
StepSizeConfig get stepSizeConfig => _stepSizeConfig;
|
|
||||||
|
|
||||||
/// Converts [domainValue] to the position to place the band/bar.
|
|
||||||
///
|
|
||||||
/// Returns 0 if not found.
|
|
||||||
@override
|
|
||||||
num operator [](String domainValue) {
|
|
||||||
if (_scaleChanged) {
|
|
||||||
_updateScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
final i = _domain.indexOf(domainValue);
|
|
||||||
if (i != null) {
|
|
||||||
return viewportTranslatePx +
|
|
||||||
_range.start +
|
|
||||||
_cachedRangeBandShift +
|
|
||||||
(_cachedStepSizePixels * i);
|
|
||||||
}
|
|
||||||
// If it wasn't found
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String reverse(double pixelLocation) {
|
|
||||||
final index = ((pixelLocation -
|
|
||||||
viewportTranslatePx -
|
|
||||||
_range.start -
|
|
||||||
_cachedRangeBandShift) /
|
|
||||||
_cachedStepSizePixels);
|
|
||||||
|
|
||||||
// The last pixel belongs in the last step even if it tries to round up.
|
|
||||||
//
|
|
||||||
// Index may be less than 0 when [pixelLocation] is less than the width of
|
|
||||||
// the range band shift. This may happen on the far left side of the chart,
|
|
||||||
// where we want the first datum anyways. Wrapping the result in "max(0, x)"
|
|
||||||
// cuts off these negative values.
|
|
||||||
return _domain
|
|
||||||
.getDomainAtIndex(max(0, min(index.round(), domain.size - 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool canTranslate(String domainValue) =>
|
|
||||||
(_domain.indexOf(domainValue) != null);
|
|
||||||
|
|
||||||
@override
|
|
||||||
OrdinalScaleDomainInfo get domain => _domain;
|
|
||||||
|
|
||||||
/// Update the scale to include [domainValue].
|
|
||||||
@override
|
|
||||||
void addDomain(String domainValue) {
|
|
||||||
_domain.add(domainValue);
|
|
||||||
_scaleChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
set range(ScaleOutputExtent extent) {
|
|
||||||
_range = extent;
|
|
||||||
_scaleChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ScaleOutputExtent get range => _range;
|
|
||||||
|
|
||||||
@override
|
|
||||||
resetDomain() {
|
|
||||||
_domain.clear();
|
|
||||||
_scaleChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
resetViewportSettings() {
|
|
||||||
_viewportScale = 1.0;
|
|
||||||
_viewportTranslatePx = 0.0;
|
|
||||||
_scaleChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get rangeWidth => (range.start - range.end).abs().toInt();
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get viewportScalingFactor => _viewportScale;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get viewportTranslatePx => _viewportTranslatePx;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setViewportSettings(double viewportScale, double viewportTranslatePx) {
|
|
||||||
_viewportScale = viewportScale;
|
|
||||||
_viewportTranslatePx =
|
|
||||||
min(0.0, max(rangeWidth * (1.0 - viewportScale), viewportTranslatePx));
|
|
||||||
|
|
||||||
_scaleChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setViewport(int viewportDataSize, String startingDomain) {
|
|
||||||
if (startingDomain != null &&
|
|
||||||
viewportDataSize != null &&
|
|
||||||
viewportDataSize <= 0) {
|
|
||||||
throw ArgumentError('viewportDataSize can' 't be less than 1.');
|
|
||||||
}
|
|
||||||
|
|
||||||
_scaleChanged = true;
|
|
||||||
_viewportDataSize = viewportDataSize;
|
|
||||||
_viewportStartingDomain = startingDomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update this scale's viewport using settings [_viewportDataSize] and
|
|
||||||
/// [_viewportStartingDomain].
|
|
||||||
void _updateViewport() {
|
|
||||||
setViewportSettings(1.0, 0.0);
|
|
||||||
_recalculateScale();
|
|
||||||
if (_domain.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the scale with zoom level to help find the correct translate.
|
|
||||||
setViewportSettings(
|
|
||||||
_domain.size / min(_viewportDataSize, _domain.size), 0.0);
|
|
||||||
_recalculateScale();
|
|
||||||
final domainIndex = _domain.indexOf(_viewportStartingDomain);
|
|
||||||
if (domainIndex != null) {
|
|
||||||
// Update the translate so that the scale starts half a step before the
|
|
||||||
// chosen domain.
|
|
||||||
final viewportTranslatePx = -(_cachedStepSizePixels * domainIndex);
|
|
||||||
setViewportSettings(_viewportScale, viewportTranslatePx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get viewportDataSize {
|
|
||||||
if (_scaleChanged) {
|
|
||||||
_updateScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _domain.isEmpty ? 0 : (rangeWidth ~/ _cachedStepSizePixels);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get viewportStartingDomain {
|
|
||||||
if (_scaleChanged) {
|
|
||||||
_updateScale();
|
|
||||||
}
|
|
||||||
if (_domain.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return _domain.getDomainAtIndex(
|
|
||||||
(-_viewportTranslatePx / _cachedStepSizePixels).ceil().toInt());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isRangeValueWithinViewport(double rangeValue) {
|
|
||||||
return range != null && rangeValue >= range.min && rangeValue <= range.max;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int compareDomainValueToViewport(String domainValue) {
|
|
||||||
// TODO: This currently works because range defaults to 0-1
|
|
||||||
// This needs to be looked into further.
|
|
||||||
var i = _domain.indexOf(domainValue);
|
|
||||||
if (i != null && range != null) {
|
|
||||||
var domainPx = this[domainValue];
|
|
||||||
if (domainPx < range.min) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (domainPx > range.max) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
SimpleOrdinalScale copy() => SimpleOrdinalScale._copy(this);
|
|
||||||
|
|
||||||
void _updateCachedFields(
|
|
||||||
double stepSizePixels, double rangeBandPixels, double rangeBandShift) {
|
|
||||||
_cachedStepSizePixels = stepSizePixels;
|
|
||||||
_cachedRangeBandSize = rangeBandPixels;
|
|
||||||
_cachedRangeBandShift = rangeBandShift;
|
|
||||||
|
|
||||||
// TODO: When there are horizontal bars increasing from where
|
|
||||||
// the domain and measure axis intersects but the desired behavior is
|
|
||||||
// flipped. The plan is to fix this by fixing code to flip the range in the
|
|
||||||
// code.
|
|
||||||
|
|
||||||
// If range start is less than range end, then the domain is calculated by
|
|
||||||
// adding the band width. If range start is greater than range end, then the
|
|
||||||
// domain is calculated by subtracting from the band width (ex. horizontal
|
|
||||||
// bar charts where first series is at the bottom of the chart).
|
|
||||||
if (range.start > range.end) {
|
|
||||||
_cachedStepSizePixels *= -1;
|
|
||||||
_cachedRangeBandShift *= -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_scaleChanged = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateScale() {
|
|
||||||
if (_viewportStartingDomain != null && _viewportDataSize != null) {
|
|
||||||
// Update viewport recalculates the scale.
|
|
||||||
_updateViewport();
|
|
||||||
}
|
|
||||||
_recalculateScale();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _recalculateScale() {
|
|
||||||
final stepSizePixels = _domain.isEmpty
|
|
||||||
? 0.0
|
|
||||||
: _viewportScale * (rangeWidth.toDouble() / _domain.size.toDouble());
|
|
||||||
double rangeBandPixels;
|
|
||||||
|
|
||||||
switch (rangeBandConfig.type) {
|
|
||||||
case RangeBandType.fixedPixel:
|
|
||||||
rangeBandPixels = rangeBandConfig.size.toDouble();
|
|
||||||
break;
|
|
||||||
case RangeBandType.fixedPixelSpaceFromStep:
|
|
||||||
var spaceInPixels = rangeBandConfig.size.toDouble();
|
|
||||||
rangeBandPixels = max(0.0, stepSizePixels - spaceInPixels);
|
|
||||||
break;
|
|
||||||
case RangeBandType.styleAssignedPercentOfStep:
|
|
||||||
case RangeBandType.fixedPercentOfStep:
|
|
||||||
var percent = rangeBandConfig.size.toDouble();
|
|
||||||
rangeBandPixels = stepSizePixels * percent;
|
|
||||||
break;
|
|
||||||
case RangeBandType.fixedDomain:
|
|
||||||
case RangeBandType.none:
|
|
||||||
default:
|
|
||||||
throw StateError('RangeBandType must not be NONE or FIXED_DOMAIN');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateCachedFields(stepSizePixels, rangeBandPixels, stepSizePixels / 2.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show immutable;
|
|
||||||
|
|
||||||
import '../../../../common/color.dart' show Color;
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../axis.dart' show Axis;
|
|
||||||
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import '../tick_formatter.dart' show TickFormatter;
|
|
||||||
import '../tick_provider.dart' show TickProvider;
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class AxisSpec<D> {
|
|
||||||
final bool showAxisLine;
|
|
||||||
final RenderSpec<D> renderSpec;
|
|
||||||
final TickProviderSpec<D> tickProviderSpec;
|
|
||||||
final TickFormatterSpec<D> tickFormatterSpec;
|
|
||||||
|
|
||||||
const AxisSpec({
|
|
||||||
this.renderSpec,
|
|
||||||
this.tickProviderSpec,
|
|
||||||
this.tickFormatterSpec,
|
|
||||||
this.showAxisLine,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory AxisSpec.from(
|
|
||||||
AxisSpec other, {
|
|
||||||
RenderSpec<D> renderSpec,
|
|
||||||
TickProviderSpec<D> tickProviderSpec,
|
|
||||||
TickFormatterSpec<D> tickFormatterSpec,
|
|
||||||
bool showAxisLine,
|
|
||||||
}) {
|
|
||||||
return AxisSpec(
|
|
||||||
renderSpec: renderSpec ?? other.renderSpec,
|
|
||||||
tickProviderSpec: tickProviderSpec ?? other.tickProviderSpec,
|
|
||||||
tickFormatterSpec: tickFormatterSpec ?? other.tickFormatterSpec,
|
|
||||||
showAxisLine: showAxisLine ?? other.showAxisLine,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
configure(
|
|
||||||
Axis<D> axis, ChartContext context, GraphicsFactory graphicsFactory) {
|
|
||||||
if (showAxisLine != null) {
|
|
||||||
axis.forceDrawAxisLine = showAxisLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (renderSpec != null) {
|
|
||||||
axis.tickDrawStrategy =
|
|
||||||
renderSpec.createDrawStrategy(context, graphicsFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tickProviderSpec != null) {
|
|
||||||
axis.tickProvider = tickProviderSpec.createTickProvider(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tickFormatterSpec != null) {
|
|
||||||
axis.tickFormatter = tickFormatterSpec.createTickFormatter(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an appropriately typed [Axis].
|
|
||||||
Axis<D> createAxis() => null;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is AxisSpec &&
|
|
||||||
renderSpec == other.renderSpec &&
|
|
||||||
tickProviderSpec == other.tickProviderSpec &&
|
|
||||||
tickFormatterSpec == other.tickFormatterSpec &&
|
|
||||||
showAxisLine == other.showAxisLine);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = renderSpec?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + tickProviderSpec.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + tickFormatterSpec.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + showAxisLine.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
abstract class TickProviderSpec<D> {
|
|
||||||
TickProvider<D> createTickProvider(ChartContext context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
abstract class TickFormatterSpec<D> {
|
|
||||||
TickFormatter<D> createTickFormatter(ChartContext context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
abstract class RenderSpec<D> {
|
|
||||||
const RenderSpec();
|
|
||||||
|
|
||||||
TickDrawStrategy<D> createDrawStrategy(
|
|
||||||
ChartContext context, GraphicsFactory graphicFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class TextStyleSpec {
|
|
||||||
final String fontFamily;
|
|
||||||
final int fontSize;
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
const TextStyleSpec({this.fontFamily, this.fontSize, this.color});
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other is TextStyleSpec &&
|
|
||||||
fontFamily == other.fontFamily &&
|
|
||||||
fontSize == other.fontSize &&
|
|
||||||
color == other.color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = fontFamily?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + fontSize?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + color?.hashCode ?? 0;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class LineStyleSpec {
|
|
||||||
final Color color;
|
|
||||||
final List<int> dashPattern;
|
|
||||||
final int thickness;
|
|
||||||
|
|
||||||
const LineStyleSpec({this.color, this.dashPattern, this.thickness});
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other is LineStyleSpec &&
|
|
||||||
color == other.color &&
|
|
||||||
dashPattern == other.dashPattern &&
|
|
||||||
thickness == other.thickness);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = color?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + dashPattern?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + thickness?.hashCode ?? 0;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TickLabelAnchor {
|
|
||||||
before,
|
|
||||||
centered,
|
|
||||||
after,
|
|
||||||
|
|
||||||
/// The top most tick draws all text under the location.
|
|
||||||
/// The bottom most tick draws all text above the location.
|
|
||||||
/// The rest of the ticks are centered.
|
|
||||||
inside,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TickLabelJustification {
|
|
||||||
inside,
|
|
||||||
outside,
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:meta/meta.dart' show immutable;
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../axis.dart' show Axis, NumericAxis;
|
|
||||||
import '../linear/bucketing_numeric_axis.dart' show BucketingNumericAxis;
|
|
||||||
import '../linear/bucketing_numeric_tick_provider.dart'
|
|
||||||
show BucketingNumericTickProvider;
|
|
||||||
import '../numeric_extents.dart' show NumericExtents;
|
|
||||||
import 'axis_spec.dart' show AxisSpec, RenderSpec;
|
|
||||||
import 'numeric_axis_spec.dart'
|
|
||||||
show
|
|
||||||
BasicNumericTickFormatterSpec,
|
|
||||||
BasicNumericTickProviderSpec,
|
|
||||||
NumericAxisSpec,
|
|
||||||
NumericTickProviderSpec,
|
|
||||||
NumericTickFormatterSpec;
|
|
||||||
|
|
||||||
/// A numeric [AxisSpec] that positions all values beneath a certain [threshold]
|
|
||||||
/// into a reserved space on the axis range. The label for the bucket line will
|
|
||||||
/// be drawn in the middle of the bucket range, rather than aligned with the
|
|
||||||
/// gridline for that value's position on the scale.
|
|
||||||
///
|
|
||||||
/// An example illustration of a bucketing measure axis on a point chart
|
|
||||||
/// follows. In this case, values such as "6%" and "3%" are drawn in the bucket
|
|
||||||
/// of the axis, since they are less than the [threshold] value of 10%.
|
|
||||||
///
|
|
||||||
/// 100% ┠─────────────────────────
|
|
||||||
/// ┃ *
|
|
||||||
/// ┃ *
|
|
||||||
/// 50% ┠──────*──────────────────
|
|
||||||
/// ┃
|
|
||||||
/// ┠─────────────────────────
|
|
||||||
/// < 10% ┃ * *
|
|
||||||
/// ┗┯━━━━━━━━━━┯━━━━━━━━━━━┯━
|
|
||||||
/// 0 50 100
|
|
||||||
///
|
|
||||||
/// This axis will format numbers as percents by default.
|
|
||||||
@immutable
|
|
||||||
class BucketingAxisSpec extends NumericAxisSpec {
|
|
||||||
/// All values smaller than the threshold will be bucketed into the same
|
|
||||||
/// position in the reserved space on the axis.
|
|
||||||
final num threshold;
|
|
||||||
|
|
||||||
/// Whether or not measure values bucketed below the [threshold] should be
|
|
||||||
/// visible on the chart, or collapsed.
|
|
||||||
///
|
|
||||||
/// If this is false, then any data with measure values smaller than
|
|
||||||
/// [threshold] will not be rendered on the chart.
|
|
||||||
final bool showBucket;
|
|
||||||
|
|
||||||
/// Creates a [NumericAxisSpec] that is specialized for percentage data.
|
|
||||||
BucketingAxisSpec({
|
|
||||||
RenderSpec<num> renderSpec,
|
|
||||||
NumericTickProviderSpec tickProviderSpec,
|
|
||||||
NumericTickFormatterSpec tickFormatterSpec,
|
|
||||||
bool showAxisLine,
|
|
||||||
bool showBucket,
|
|
||||||
this.threshold,
|
|
||||||
NumericExtents viewport,
|
|
||||||
}) : this.showBucket = showBucket ?? true,
|
|
||||||
super(
|
|
||||||
renderSpec: renderSpec,
|
|
||||||
tickProviderSpec:
|
|
||||||
tickProviderSpec ?? const BucketingNumericTickProviderSpec(),
|
|
||||||
tickFormatterSpec: tickFormatterSpec ??
|
|
||||||
BasicNumericTickFormatterSpec.fromNumberFormat(
|
|
||||||
NumberFormat.percentPattern()),
|
|
||||||
showAxisLine: showAxisLine,
|
|
||||||
viewport: viewport ?? const NumericExtents(0.0, 1.0));
|
|
||||||
|
|
||||||
@override
|
|
||||||
configure(
|
|
||||||
Axis<num> axis, ChartContext context, GraphicsFactory graphicsFactory) {
|
|
||||||
super.configure(axis, context, graphicsFactory);
|
|
||||||
|
|
||||||
if (axis is NumericAxis && viewport != null) {
|
|
||||||
axis.setScaleViewport(viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (axis is BucketingNumericAxis && threshold != null) {
|
|
||||||
axis.threshold = threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (axis is BucketingNumericAxis && showBucket != null) {
|
|
||||||
axis.showBucket = showBucket;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
BucketingNumericAxis createAxis() => BucketingNumericAxis();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is BucketingAxisSpec &&
|
|
||||||
showBucket == other.showBucket &&
|
|
||||||
threshold == other.threshold &&
|
|
||||||
super == (other));
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = super.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + showBucket.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + threshold.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class BucketingNumericTickProviderSpec extends BasicNumericTickProviderSpec {
|
|
||||||
/// Creates a [TickProviderSpec] that generates ticks for a bucketing axis.
|
|
||||||
///
|
|
||||||
/// [zeroBound] automatically include zero in the data range.
|
|
||||||
/// [dataIsInWholeNumbers] skip over ticks that would produce
|
|
||||||
/// fractional ticks that don't make sense for the domain (ie: headcount).
|
|
||||||
/// [desiredTickCount] the fixed number of ticks to try to make. Convenience
|
|
||||||
/// that sets [desiredMinTickCount] and [desiredMaxTickCount] the same.
|
|
||||||
/// Both min and max win out if they are set along with
|
|
||||||
/// [desiredTickCount].
|
|
||||||
/// [desiredMinTickCount] automatically choose the best tick
|
|
||||||
/// count to produce the 'nicest' ticks but make sure we have this many.
|
|
||||||
/// [desiredMaxTickCount] automatically choose the best tick
|
|
||||||
/// count to produce the 'nicest' ticks but make sure we don't have more
|
|
||||||
/// than this many.
|
|
||||||
const BucketingNumericTickProviderSpec(
|
|
||||||
{bool zeroBound,
|
|
||||||
bool dataIsInWholeNumbers,
|
|
||||||
int desiredTickCount,
|
|
||||||
int desiredMinTickCount,
|
|
||||||
int desiredMaxTickCount})
|
|
||||||
: super(
|
|
||||||
zeroBound: zeroBound ?? true,
|
|
||||||
dataIsInWholeNumbers: dataIsInWholeNumbers ?? false,
|
|
||||||
desiredTickCount: desiredTickCount,
|
|
||||||
desiredMinTickCount: desiredMinTickCount,
|
|
||||||
desiredMaxTickCount: desiredMaxTickCount,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
BucketingNumericTickProvider createTickProvider(ChartContext context) {
|
|
||||||
final provider = BucketingNumericTickProvider()
|
|
||||||
..zeroBound = zeroBound
|
|
||||||
..dataIsInWholeNumbers = dataIsInWholeNumbers;
|
|
||||||
|
|
||||||
if (desiredMinTickCount != null ||
|
|
||||||
desiredMaxTickCount != null ||
|
|
||||||
desiredTickCount != null) {
|
|
||||||
provider.setTickCount(desiredMaxTickCount ?? desiredTickCount ?? 10,
|
|
||||||
desiredMinTickCount ?? desiredTickCount ?? 2);
|
|
||||||
}
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show immutable;
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../axis.dart' show Axis;
|
|
||||||
import '../end_points_tick_provider.dart' show EndPointsTickProvider;
|
|
||||||
import '../static_tick_provider.dart' show StaticTickProvider;
|
|
||||||
import '../time/auto_adjusting_date_time_tick_provider.dart'
|
|
||||||
show AutoAdjustingDateTimeTickProvider;
|
|
||||||
import '../time/date_time_axis.dart' show DateTimeAxis;
|
|
||||||
import '../time/date_time_extents.dart' show DateTimeExtents;
|
|
||||||
import '../time/date_time_tick_formatter.dart' show DateTimeTickFormatter;
|
|
||||||
import '../time/day_time_stepper.dart' show DayTimeStepper;
|
|
||||||
import '../time/hour_tick_formatter.dart' show HourTickFormatter;
|
|
||||||
import '../time/time_range_tick_provider_impl.dart'
|
|
||||||
show TimeRangeTickProviderImpl;
|
|
||||||
import '../time/time_tick_formatter.dart' show TimeTickFormatter;
|
|
||||||
import '../time/time_tick_formatter_impl.dart'
|
|
||||||
show CalendarField, TimeTickFormatterImpl;
|
|
||||||
import 'axis_spec.dart'
|
|
||||||
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
|
|
||||||
import 'tick_spec.dart' show TickSpec;
|
|
||||||
|
|
||||||
/// Generic [AxisSpec] specialized for Timeseries charts.
|
|
||||||
@immutable
|
|
||||||
class DateTimeAxisSpec extends AxisSpec<DateTime> {
|
|
||||||
/// Sets viewport for this Axis.
|
|
||||||
///
|
|
||||||
/// If pan / zoom behaviors are set, this is the initial viewport.
|
|
||||||
final DateTimeExtents viewport;
|
|
||||||
|
|
||||||
/// Creates a [AxisSpec] that specialized for timeseries charts.
|
|
||||||
///
|
|
||||||
/// [renderSpec] spec used to configure how the ticks and labels
|
|
||||||
/// actually render. Possible values are [GridlineRendererSpec],
|
|
||||||
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
|
|
||||||
/// given to the RenderSpec is of type [DateTime] for Timeseries.
|
|
||||||
/// [tickProviderSpec] spec used to configure what ticks are generated.
|
|
||||||
/// [tickFormatterSpec] spec used to configure how the tick labels
|
|
||||||
/// are formatted.
|
|
||||||
/// [showAxisLine] override to force the axis to draw the axis
|
|
||||||
/// line.
|
|
||||||
const DateTimeAxisSpec({
|
|
||||||
RenderSpec<DateTime> renderSpec,
|
|
||||||
DateTimeTickProviderSpec tickProviderSpec,
|
|
||||||
DateTimeTickFormatterSpec tickFormatterSpec,
|
|
||||||
bool showAxisLine,
|
|
||||||
this.viewport,
|
|
||||||
}) : super(
|
|
||||||
renderSpec: renderSpec,
|
|
||||||
tickProviderSpec: tickProviderSpec,
|
|
||||||
tickFormatterSpec: tickFormatterSpec,
|
|
||||||
showAxisLine: showAxisLine);
|
|
||||||
|
|
||||||
@override
|
|
||||||
configure(Axis<DateTime> axis, ChartContext context,
|
|
||||||
GraphicsFactory graphicsFactory) {
|
|
||||||
super.configure(axis, context, graphicsFactory);
|
|
||||||
|
|
||||||
if (axis is DateTimeAxis && viewport != null) {
|
|
||||||
axis.setScaleViewport(viewport);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Axis<DateTime> createAxis() {
|
|
||||||
assert(false, 'Call createDateTimeAxis() to create a DateTimeAxis.');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [DateTimeAxis]. This should be called in place of createAxis.
|
|
||||||
DateTimeAxis createDateTimeAxis(DateTimeFactory dateTimeFactory) =>
|
|
||||||
DateTimeAxis(dateTimeFactory);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is DateTimeAxisSpec &&
|
|
||||||
viewport == other.viewport &&
|
|
||||||
super == (other);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = super.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + viewport.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class DateTimeTickProviderSpec extends TickProviderSpec<DateTime> {}
|
|
||||||
|
|
||||||
abstract class DateTimeTickFormatterSpec extends TickFormatterSpec<DateTime> {}
|
|
||||||
|
|
||||||
/// [TickProviderSpec] that sets up the automatically assigned time ticks based
|
|
||||||
/// on the extents of your data.
|
|
||||||
@immutable
|
|
||||||
class AutoDateTimeTickProviderSpec implements DateTimeTickProviderSpec {
|
|
||||||
final bool includeTime;
|
|
||||||
|
|
||||||
/// Creates a [TickProviderSpec] that dynamically chooses ticks based on the
|
|
||||||
/// extents of the data.
|
|
||||||
///
|
|
||||||
/// [includeTime] - flag that indicates whether the time should be
|
|
||||||
/// included when choosing appropriate tick intervals.
|
|
||||||
const AutoDateTimeTickProviderSpec({this.includeTime = true});
|
|
||||||
|
|
||||||
@override
|
|
||||||
AutoAdjustingDateTimeTickProvider createTickProvider(ChartContext context) {
|
|
||||||
if (includeTime) {
|
|
||||||
return AutoAdjustingDateTimeTickProvider.createDefault(
|
|
||||||
context.dateTimeFactory);
|
|
||||||
} else {
|
|
||||||
return AutoAdjustingDateTimeTickProvider.createWithoutTime(
|
|
||||||
context.dateTimeFactory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is AutoDateTimeTickProviderSpec && includeTime == other.includeTime;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => includeTime?.hashCode ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [TickProviderSpec] that sets up time ticks with days increments only.
|
|
||||||
@immutable
|
|
||||||
class DayTickProviderSpec implements DateTimeTickProviderSpec {
|
|
||||||
final List<int> increments;
|
|
||||||
|
|
||||||
const DayTickProviderSpec({this.increments});
|
|
||||||
|
|
||||||
/// Creates a [TickProviderSpec] that dynamically chooses ticks based on the
|
|
||||||
/// extents of the data, limited to day increments.
|
|
||||||
///
|
|
||||||
/// [increments] specify the number of day increments that can be chosen from
|
|
||||||
/// when searching for the appropriate tick intervals.
|
|
||||||
@override
|
|
||||||
AutoAdjustingDateTimeTickProvider createTickProvider(ChartContext context) {
|
|
||||||
return AutoAdjustingDateTimeTickProvider.createWith([
|
|
||||||
TimeRangeTickProviderImpl(DayTimeStepper(context.dateTimeFactory,
|
|
||||||
allowedTickIncrements: increments))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is DayTickProviderSpec && increments == other.increments;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => increments?.hashCode ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [TickProviderSpec] that sets up time ticks at the two end points of the axis
|
|
||||||
/// range.
|
|
||||||
@immutable
|
|
||||||
class DateTimeEndPointsTickProviderSpec implements DateTimeTickProviderSpec {
|
|
||||||
const DateTimeEndPointsTickProviderSpec();
|
|
||||||
|
|
||||||
/// Creates a [TickProviderSpec] that dynamically chooses time ticks at the
|
|
||||||
/// two end points of the axis range
|
|
||||||
@override
|
|
||||||
EndPointsTickProvider<DateTime> createTickProvider(ChartContext context) {
|
|
||||||
return EndPointsTickProvider<DateTime>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => other is DateTimeEndPointsTickProviderSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [TickProviderSpec] that allows you to specific the ticks to be used.
|
|
||||||
@immutable
|
|
||||||
class StaticDateTimeTickProviderSpec implements DateTimeTickProviderSpec {
|
|
||||||
final List<TickSpec<DateTime>> tickSpecs;
|
|
||||||
|
|
||||||
const StaticDateTimeTickProviderSpec(this.tickSpecs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
StaticTickProvider<DateTime> createTickProvider(ChartContext context) =>
|
|
||||||
StaticTickProvider<DateTime>(tickSpecs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is StaticDateTimeTickProviderSpec && tickSpecs == other.tickSpecs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => tickSpecs.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formatters for a single level of the [DateTimeTickFormatterSpec].
|
|
||||||
@immutable
|
|
||||||
class TimeFormatterSpec {
|
|
||||||
final String format;
|
|
||||||
final String transitionFormat;
|
|
||||||
final String noonFormat;
|
|
||||||
|
|
||||||
/// Creates a formatter for a particular granularity of data.
|
|
||||||
///
|
|
||||||
/// [format] [DateFormat] format string used to format non-transition ticks.
|
|
||||||
/// The string is given to the dateTimeFactory to support i18n formatting.
|
|
||||||
/// [transitionFormat] [DateFormat] format string used to format transition
|
|
||||||
/// ticks. Examples of transition ticks:
|
|
||||||
/// Day ticks would have a transition tick at month boundaries.
|
|
||||||
/// Hour ticks would have a transition tick at day boundaries.
|
|
||||||
/// The first tick is typically a transition tick.
|
|
||||||
/// [noonFormat] [DateFormat] format string used only for formatting hours
|
|
||||||
/// in the event that you want to format noon differently than other
|
|
||||||
/// hours (ie: [10, 11, 12p, 1, 2, 3]).
|
|
||||||
const TimeFormatterSpec(
|
|
||||||
{this.format, this.transitionFormat, this.noonFormat});
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is TimeFormatterSpec &&
|
|
||||||
format == other.format &&
|
|
||||||
transitionFormat == other.transitionFormat &&
|
|
||||||
noonFormat == other.noonFormat;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = format?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + transitionFormat?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + noonFormat?.hashCode ?? 0;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [TickFormatterSpec] that automatically chooses the appropriate level of
|
|
||||||
/// formatting based on the tick stepSize. Each level of date granularity has
|
|
||||||
/// its own [TimeFormatterSpec] used to specify the formatting strings at that
|
|
||||||
/// level.
|
|
||||||
@immutable
|
|
||||||
class AutoDateTimeTickFormatterSpec implements DateTimeTickFormatterSpec {
|
|
||||||
final TimeFormatterSpec minute;
|
|
||||||
final TimeFormatterSpec hour;
|
|
||||||
final TimeFormatterSpec day;
|
|
||||||
final TimeFormatterSpec month;
|
|
||||||
final TimeFormatterSpec year;
|
|
||||||
|
|
||||||
/// Creates a [TickFormatterSpec] that automatically chooses the formatting
|
|
||||||
/// given the individual [TimeFormatterSpec] formatters that are set.
|
|
||||||
///
|
|
||||||
/// There is a default formatter for each level that is configurable, but
|
|
||||||
/// by specifying a level here it replaces the default for that particular
|
|
||||||
/// granularity. This is useful for swapping out one or all of the formatters.
|
|
||||||
const AutoDateTimeTickFormatterSpec(
|
|
||||||
{this.minute, this.hour, this.day, this.month, this.year});
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTimeTickFormatter createTickFormatter(ChartContext context) {
|
|
||||||
final Map<int, TimeTickFormatter> map = {};
|
|
||||||
|
|
||||||
if (minute != null) {
|
|
||||||
map[DateTimeTickFormatter.MINUTE] =
|
|
||||||
_makeFormatter(minute, CalendarField.hourOfDay, context);
|
|
||||||
}
|
|
||||||
if (hour != null) {
|
|
||||||
map[DateTimeTickFormatter.HOUR] =
|
|
||||||
_makeFormatter(hour, CalendarField.date, context);
|
|
||||||
}
|
|
||||||
if (day != null) {
|
|
||||||
map[23 * DateTimeTickFormatter.HOUR] =
|
|
||||||
_makeFormatter(day, CalendarField.month, context);
|
|
||||||
}
|
|
||||||
if (month != null) {
|
|
||||||
map[28 * DateTimeTickFormatter.DAY] =
|
|
||||||
_makeFormatter(month, CalendarField.year, context);
|
|
||||||
}
|
|
||||||
if (year != null) {
|
|
||||||
map[364 * DateTimeTickFormatter.DAY] =
|
|
||||||
_makeFormatter(year, CalendarField.year, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTimeTickFormatter(context.dateTimeFactory, overrides: map);
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeTickFormatterImpl _makeFormatter(TimeFormatterSpec spec,
|
|
||||||
CalendarField transitionField, ChartContext context) {
|
|
||||||
if (spec.noonFormat != null) {
|
|
||||||
return HourTickFormatter(
|
|
||||||
dateTimeFactory: context.dateTimeFactory,
|
|
||||||
simpleFormat: spec.format,
|
|
||||||
transitionFormat: spec.transitionFormat,
|
|
||||||
noonFormat: spec.noonFormat);
|
|
||||||
} else {
|
|
||||||
return TimeTickFormatterImpl(
|
|
||||||
dateTimeFactory: context.dateTimeFactory,
|
|
||||||
simpleFormat: spec.format,
|
|
||||||
transitionFormat: spec.transitionFormat,
|
|
||||||
transitionField: transitionField);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is AutoDateTimeTickFormatterSpec &&
|
|
||||||
minute == other.minute &&
|
|
||||||
hour == other.hour &&
|
|
||||||
day == other.day &&
|
|
||||||
month == other.month &&
|
|
||||||
year == other.year);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = minute?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + hour?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + day?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + month?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + year?.hashCode ?? 0;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show immutable;
|
|
||||||
|
|
||||||
import '../draw_strategy/small_tick_draw_strategy.dart'
|
|
||||||
show SmallTickRendererSpec;
|
|
||||||
import '../time/date_time_extents.dart' show DateTimeExtents;
|
|
||||||
import 'axis_spec.dart' show AxisSpec, RenderSpec, TickLabelAnchor;
|
|
||||||
import 'date_time_axis_spec.dart'
|
|
||||||
show
|
|
||||||
DateTimeAxisSpec,
|
|
||||||
DateTimeEndPointsTickProviderSpec,
|
|
||||||
DateTimeTickFormatterSpec,
|
|
||||||
DateTimeTickProviderSpec;
|
|
||||||
|
|
||||||
/// Default [AxisSpec] used for Timeseries charts.
|
|
||||||
@immutable
|
|
||||||
class EndPointsTimeAxisSpec extends DateTimeAxisSpec {
|
|
||||||
/// Creates a [AxisSpec] that specialized for timeseries charts.
|
|
||||||
///
|
|
||||||
/// [renderSpec] spec used to configure how the ticks and labels
|
|
||||||
/// actually render. Possible values are [GridlineRendererSpec],
|
|
||||||
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
|
|
||||||
/// given to the RenderSpec is of type [DateTime] for Timeseries.
|
|
||||||
/// [tickProviderSpec] spec used to configure what ticks are generated.
|
|
||||||
/// [tickFormatterSpec] spec used to configure how the tick labels
|
|
||||||
/// are formatted.
|
|
||||||
/// [showAxisLine] override to force the axis to draw the axis
|
|
||||||
/// line.
|
|
||||||
const EndPointsTimeAxisSpec({
|
|
||||||
RenderSpec<DateTime> renderSpec,
|
|
||||||
DateTimeTickProviderSpec tickProviderSpec,
|
|
||||||
DateTimeTickFormatterSpec tickFormatterSpec,
|
|
||||||
bool showAxisLine,
|
|
||||||
DateTimeExtents viewport,
|
|
||||||
bool usingBarRenderer = false,
|
|
||||||
}) : super(
|
|
||||||
renderSpec: renderSpec ??
|
|
||||||
const SmallTickRendererSpec<DateTime>(
|
|
||||||
labelAnchor: TickLabelAnchor.inside,
|
|
||||||
labelOffsetFromTickPx: 0),
|
|
||||||
tickProviderSpec:
|
|
||||||
tickProviderSpec ?? const DateTimeEndPointsTickProviderSpec(),
|
|
||||||
tickFormatterSpec: tickFormatterSpec,
|
|
||||||
showAxisLine: showAxisLine,
|
|
||||||
viewport: viewport);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is EndPointsTimeAxisSpec && super == (other));
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:charts_common/src/chart/cartesian/axis/tick_formatter.dart';
|
|
||||||
import 'package:meta/meta.dart' show immutable;
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../../../common/datum_details.dart' show MeasureFormatter;
|
|
||||||
import '../axis.dart' show Axis, NumericAxis;
|
|
||||||
import '../end_points_tick_provider.dart' show EndPointsTickProvider;
|
|
||||||
import '../numeric_extents.dart' show NumericExtents;
|
|
||||||
import '../numeric_tick_provider.dart' show NumericTickProvider;
|
|
||||||
import '../static_tick_provider.dart' show StaticTickProvider;
|
|
||||||
import '../tick_formatter.dart' show NumericTickFormatter;
|
|
||||||
import 'axis_spec.dart'
|
|
||||||
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
|
|
||||||
import 'tick_spec.dart' show TickSpec;
|
|
||||||
|
|
||||||
/// [AxisSpec] specialized for numeric/continuous axes like the measure axis.
|
|
||||||
@immutable
|
|
||||||
class NumericAxisSpec extends AxisSpec<num> {
|
|
||||||
/// Sets viewport for this Axis.
|
|
||||||
///
|
|
||||||
/// If pan / zoom behaviors are set, this is the initial viewport.
|
|
||||||
final NumericExtents viewport;
|
|
||||||
|
|
||||||
/// Creates a [AxisSpec] that specialized for numeric data.
|
|
||||||
///
|
|
||||||
/// [renderSpec] spec used to configure how the ticks and labels
|
|
||||||
/// actually render. Possible values are [GridlineRendererSpec],
|
|
||||||
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
|
|
||||||
/// given to the RenderSpec is of type [num] when using this spec.
|
|
||||||
/// [tickProviderSpec] spec used to configure what ticks are generated.
|
|
||||||
/// [tickFormatterSpec] spec used to configure how the tick labels are
|
|
||||||
/// formatted.
|
|
||||||
/// [showAxisLine] override to force the axis to draw the axis line.
|
|
||||||
const NumericAxisSpec({
|
|
||||||
RenderSpec<num> renderSpec,
|
|
||||||
NumericTickProviderSpec tickProviderSpec,
|
|
||||||
NumericTickFormatterSpec tickFormatterSpec,
|
|
||||||
bool showAxisLine,
|
|
||||||
this.viewport,
|
|
||||||
}) : super(
|
|
||||||
renderSpec: renderSpec,
|
|
||||||
tickProviderSpec: tickProviderSpec,
|
|
||||||
tickFormatterSpec: tickFormatterSpec,
|
|
||||||
showAxisLine: showAxisLine);
|
|
||||||
|
|
||||||
factory NumericAxisSpec.from(
|
|
||||||
NumericAxisSpec other, {
|
|
||||||
RenderSpec<num> renderSpec,
|
|
||||||
TickProviderSpec tickProviderSpec,
|
|
||||||
TickFormatterSpec tickFormatterSpec,
|
|
||||||
bool showAxisLine,
|
|
||||||
NumericExtents viewport,
|
|
||||||
}) {
|
|
||||||
return NumericAxisSpec(
|
|
||||||
renderSpec: renderSpec ?? other.renderSpec,
|
|
||||||
tickProviderSpec: tickProviderSpec ?? other.tickProviderSpec,
|
|
||||||
tickFormatterSpec: tickFormatterSpec ?? other.tickFormatterSpec,
|
|
||||||
showAxisLine: showAxisLine ?? other.showAxisLine,
|
|
||||||
viewport: viewport ?? other.viewport,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
configure(
|
|
||||||
Axis<num> axis, ChartContext context, GraphicsFactory graphicsFactory) {
|
|
||||||
super.configure(axis, context, graphicsFactory);
|
|
||||||
|
|
||||||
if (axis is NumericAxis && viewport != null) {
|
|
||||||
axis.setScaleViewport(viewport);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
NumericAxis createAxis() => NumericAxis();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is NumericAxisSpec &&
|
|
||||||
viewport == other.viewport &&
|
|
||||||
super == (other);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = super.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + viewport.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + super.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class NumericTickProviderSpec extends TickProviderSpec<num> {}
|
|
||||||
|
|
||||||
abstract class NumericTickFormatterSpec extends TickFormatterSpec<num> {}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class BasicNumericTickProviderSpec implements NumericTickProviderSpec {
|
|
||||||
final bool zeroBound;
|
|
||||||
final bool dataIsInWholeNumbers;
|
|
||||||
final int desiredTickCount;
|
|
||||||
final int desiredMinTickCount;
|
|
||||||
final int desiredMaxTickCount;
|
|
||||||
|
|
||||||
/// Creates a [TickProviderSpec] that dynamically chooses the number of
|
|
||||||
/// ticks based on the extents of the data.
|
|
||||||
///
|
|
||||||
/// [zeroBound] automatically include zero in the data range.
|
|
||||||
/// [dataIsInWholeNumbers] skip over ticks that would produce
|
|
||||||
/// fractional ticks that don't make sense for the domain (ie: headcount).
|
|
||||||
/// [desiredTickCount] the fixed number of ticks to try to make. Convenience
|
|
||||||
/// that sets [desiredMinTickCount] and [desiredMaxTickCount] the same.
|
|
||||||
/// Both min and max win out if they are set along with
|
|
||||||
/// [desiredTickCount].
|
|
||||||
/// [desiredMinTickCount] automatically choose the best tick
|
|
||||||
/// count to produce the 'nicest' ticks but make sure we have this many.
|
|
||||||
/// [desiredMaxTickCount] automatically choose the best tick
|
|
||||||
/// count to produce the 'nicest' ticks but make sure we don't have more
|
|
||||||
/// than this many.
|
|
||||||
const BasicNumericTickProviderSpec(
|
|
||||||
{this.zeroBound,
|
|
||||||
this.dataIsInWholeNumbers,
|
|
||||||
this.desiredTickCount,
|
|
||||||
this.desiredMinTickCount,
|
|
||||||
this.desiredMaxTickCount});
|
|
||||||
|
|
||||||
@override
|
|
||||||
NumericTickProvider createTickProvider(ChartContext context) {
|
|
||||||
final provider = NumericTickProvider();
|
|
||||||
if (zeroBound != null) {
|
|
||||||
provider.zeroBound = zeroBound;
|
|
||||||
}
|
|
||||||
if (dataIsInWholeNumbers != null) {
|
|
||||||
provider.dataIsInWholeNumbers = dataIsInWholeNumbers;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (desiredMinTickCount != null ||
|
|
||||||
desiredMaxTickCount != null ||
|
|
||||||
desiredTickCount != null) {
|
|
||||||
provider.setTickCount(desiredMaxTickCount ?? desiredTickCount ?? 10,
|
|
||||||
desiredMinTickCount ?? desiredTickCount ?? 2);
|
|
||||||
}
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is BasicNumericTickProviderSpec &&
|
|
||||||
zeroBound == other.zeroBound &&
|
|
||||||
dataIsInWholeNumbers == other.dataIsInWholeNumbers &&
|
|
||||||
desiredTickCount == other.desiredTickCount &&
|
|
||||||
desiredMinTickCount == other.desiredMinTickCount &&
|
|
||||||
desiredMaxTickCount == other.desiredMaxTickCount;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = zeroBound?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + dataIsInWholeNumbers?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + desiredTickCount?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + desiredMinTickCount?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + desiredMaxTickCount?.hashCode ?? 0;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [TickProviderSpec] that sets up numeric ticks at the two end points of the
|
|
||||||
/// axis range.
|
|
||||||
@immutable
|
|
||||||
class NumericEndPointsTickProviderSpec implements NumericTickProviderSpec {
|
|
||||||
/// Creates a [TickProviderSpec] that dynamically chooses numeric ticks at the
|
|
||||||
/// two end points of the axis range
|
|
||||||
const NumericEndPointsTickProviderSpec();
|
|
||||||
|
|
||||||
@override
|
|
||||||
EndPointsTickProvider<num> createTickProvider(ChartContext context) {
|
|
||||||
return EndPointsTickProvider<num>();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => other is NumericEndPointsTickProviderSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [TickProviderSpec] that allows you to specific the ticks to be used.
|
|
||||||
@immutable
|
|
||||||
class StaticNumericTickProviderSpec implements NumericTickProviderSpec {
|
|
||||||
final List<TickSpec<num>> tickSpecs;
|
|
||||||
|
|
||||||
const StaticNumericTickProviderSpec(this.tickSpecs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
StaticTickProvider<num> createTickProvider(ChartContext context) =>
|
|
||||||
StaticTickProvider<num>(tickSpecs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is StaticNumericTickProviderSpec && tickSpecs == other.tickSpecs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => tickSpecs.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class BasicNumericTickFormatterSpec implements NumericTickFormatterSpec {
|
|
||||||
final MeasureFormatter formatter;
|
|
||||||
final NumberFormat numberFormat;
|
|
||||||
|
|
||||||
/// Simple [TickFormatterSpec] that delegates formatting to the given
|
|
||||||
/// [NumberFormat].
|
|
||||||
const BasicNumericTickFormatterSpec(this.formatter) : numberFormat = null;
|
|
||||||
|
|
||||||
const BasicNumericTickFormatterSpec.fromNumberFormat(this.numberFormat)
|
|
||||||
: formatter = null;
|
|
||||||
|
|
||||||
/// A formatter will be created with the number format if it is not null.
|
|
||||||
/// Otherwise, it will create one with the [MeasureFormatter] callback.
|
|
||||||
@override
|
|
||||||
NumericTickFormatter createTickFormatter(ChartContext context) {
|
|
||||||
return numberFormat != null
|
|
||||||
? NumericTickFormatter.fromNumberFormat(numberFormat)
|
|
||||||
: NumericTickFormatter(formatter: formatter);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other is BasicNumericTickFormatterSpec &&
|
|
||||||
formatter == other.formatter &&
|
|
||||||
numberFormat == other.numberFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = formatter.hashCode;
|
|
||||||
hashcode = (hashcode * 37) * numberFormat.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show immutable;
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../axis.dart' show Axis, OrdinalAxis, OrdinalViewport;
|
|
||||||
import '../ordinal_tick_provider.dart' show OrdinalTickProvider;
|
|
||||||
import '../static_tick_provider.dart' show StaticTickProvider;
|
|
||||||
import '../tick_formatter.dart' show OrdinalTickFormatter;
|
|
||||||
import 'axis_spec.dart'
|
|
||||||
show AxisSpec, TickProviderSpec, TickFormatterSpec, RenderSpec;
|
|
||||||
import 'tick_spec.dart' show TickSpec;
|
|
||||||
|
|
||||||
/// [AxisSpec] specialized for ordinal/non-continuous axes typically for bars.
|
|
||||||
@immutable
|
|
||||||
class OrdinalAxisSpec extends AxisSpec<String> {
|
|
||||||
/// Sets viewport for this Axis.
|
|
||||||
///
|
|
||||||
/// If pan / zoom behaviors are set, this is the initial viewport.
|
|
||||||
final OrdinalViewport viewport;
|
|
||||||
|
|
||||||
/// Creates a [AxisSpec] that specialized for ordinal domain charts.
|
|
||||||
///
|
|
||||||
/// [renderSpec] spec used to configure how the ticks and labels
|
|
||||||
/// actually render. Possible values are [GridlineRendererSpec],
|
|
||||||
/// [SmallTickRendererSpec] & [NoneRenderSpec]. Make sure that the <D>
|
|
||||||
/// given to the RenderSpec is of type [String] when using this spec.
|
|
||||||
/// [tickProviderSpec] spec used to configure what ticks are generated.
|
|
||||||
/// [tickFormatterSpec] spec used to configure how the tick labels are
|
|
||||||
/// formatted.
|
|
||||||
/// [showAxisLine] override to force the axis to draw the axis line.
|
|
||||||
const OrdinalAxisSpec({
|
|
||||||
RenderSpec<String> renderSpec,
|
|
||||||
OrdinalTickProviderSpec tickProviderSpec,
|
|
||||||
OrdinalTickFormatterSpec tickFormatterSpec,
|
|
||||||
bool showAxisLine,
|
|
||||||
this.viewport,
|
|
||||||
}) : super(
|
|
||||||
renderSpec: renderSpec,
|
|
||||||
tickProviderSpec: tickProviderSpec,
|
|
||||||
tickFormatterSpec: tickFormatterSpec,
|
|
||||||
showAxisLine: showAxisLine);
|
|
||||||
|
|
||||||
@override
|
|
||||||
configure(Axis<String> axis, ChartContext context,
|
|
||||||
GraphicsFactory graphicsFactory) {
|
|
||||||
super.configure(axis, context, graphicsFactory);
|
|
||||||
|
|
||||||
if (axis is OrdinalAxis && viewport != null) {
|
|
||||||
axis.setScaleViewport(viewport);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
OrdinalAxis createAxis() => OrdinalAxis();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return identical(this, other) ||
|
|
||||||
(other is OrdinalAxisSpec &&
|
|
||||||
viewport == other.viewport &&
|
|
||||||
super == (other));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = super.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + viewport.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class OrdinalTickProviderSpec extends TickProviderSpec<String> {}
|
|
||||||
|
|
||||||
abstract class OrdinalTickFormatterSpec extends TickFormatterSpec<String> {}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class BasicOrdinalTickProviderSpec implements OrdinalTickProviderSpec {
|
|
||||||
const BasicOrdinalTickProviderSpec();
|
|
||||||
|
|
||||||
@override
|
|
||||||
OrdinalTickProvider createTickProvider(ChartContext context) =>
|
|
||||||
OrdinalTickProvider();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => other is BasicOrdinalTickProviderSpec;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => 37;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [TickProviderSpec] that allows you to specific the ticks to be used.
|
|
||||||
@immutable
|
|
||||||
class StaticOrdinalTickProviderSpec implements OrdinalTickProviderSpec {
|
|
||||||
final List<TickSpec<String>> tickSpecs;
|
|
||||||
|
|
||||||
const StaticOrdinalTickProviderSpec(this.tickSpecs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
StaticTickProvider<String> createTickProvider(ChartContext context) =>
|
|
||||||
StaticTickProvider<String>(tickSpecs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is StaticOrdinalTickProviderSpec && tickSpecs == other.tickSpecs);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => tickSpecs.hashCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
|
||||||
class BasicOrdinalTickFormatterSpec implements OrdinalTickFormatterSpec {
|
|
||||||
const BasicOrdinalTickFormatterSpec();
|
|
||||||
|
|
||||||
@override
|
|
||||||
OrdinalTickFormatter createTickFormatter(ChartContext context) =>
|
|
||||||
OrdinalTickFormatter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => other is BasicOrdinalTickFormatterSpec;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => 37;
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show immutable;
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
import '../numeric_extents.dart' show NumericExtents;
|
|
||||||
import 'axis_spec.dart' show AxisSpec, RenderSpec;
|
|
||||||
import 'numeric_axis_spec.dart'
|
|
||||||
show
|
|
||||||
BasicNumericTickFormatterSpec,
|
|
||||||
BasicNumericTickProviderSpec,
|
|
||||||
NumericAxisSpec,
|
|
||||||
NumericTickProviderSpec,
|
|
||||||
NumericTickFormatterSpec;
|
|
||||||
|
|
||||||
/// Convenience [AxisSpec] specialized for numeric percentage axes.
|
|
||||||
@immutable
|
|
||||||
class PercentAxisSpec extends NumericAxisSpec {
|
|
||||||
/// Creates a [NumericAxisSpec] that is specialized for percentage data.
|
|
||||||
PercentAxisSpec({
|
|
||||||
RenderSpec<num> renderSpec,
|
|
||||||
NumericTickProviderSpec tickProviderSpec,
|
|
||||||
NumericTickFormatterSpec tickFormatterSpec,
|
|
||||||
bool showAxisLine,
|
|
||||||
NumericExtents viewport,
|
|
||||||
}) : super(
|
|
||||||
renderSpec: renderSpec,
|
|
||||||
tickProviderSpec: tickProviderSpec ??
|
|
||||||
const BasicNumericTickProviderSpec(dataIsInWholeNumbers: false),
|
|
||||||
tickFormatterSpec: tickFormatterSpec ??
|
|
||||||
BasicNumericTickFormatterSpec.fromNumberFormat(
|
|
||||||
NumberFormat.percentPattern()),
|
|
||||||
showAxisLine: showAxisLine,
|
|
||||||
viewport: viewport ?? const NumericExtents(0.0, 1.0));
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
other is PercentAxisSpec &&
|
|
||||||
viewport == other.viewport &&
|
|
||||||
super == (other);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'axis_spec.dart' show TextStyleSpec;
|
|
||||||
|
|
||||||
/// Definition for a tick.
|
|
||||||
///
|
|
||||||
/// Used to define a tick that is used by static tick provider.
|
|
||||||
class TickSpec<D> {
|
|
||||||
final D value;
|
|
||||||
final String label;
|
|
||||||
final TextStyleSpec style;
|
|
||||||
|
|
||||||
/// [value] the value of this tick
|
|
||||||
/// [label] optional label for this tick. If not set, uses the tick formatter
|
|
||||||
/// of the axis.
|
|
||||||
/// [style] optional style for this tick. If not set, uses the style of the
|
|
||||||
/// axis.
|
|
||||||
const TickSpec(this.value, {this.label, this.style});
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../common/chart_context.dart' show ChartContext;
|
|
||||||
import 'axis.dart' show AxisOrientation;
|
|
||||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import 'numeric_scale.dart' show NumericScale;
|
|
||||||
import 'scale.dart' show MutableScale;
|
|
||||||
import 'spec/tick_spec.dart' show TickSpec;
|
|
||||||
import 'tick.dart' show Tick;
|
|
||||||
import 'tick_formatter.dart' show TickFormatter;
|
|
||||||
import 'tick_provider.dart' show TickProvider, TickHint;
|
|
||||||
import 'time/date_time_scale.dart' show DateTimeScale;
|
|
||||||
|
|
||||||
/// A strategy that uses the ticks provided and only assigns positioning.
|
|
||||||
///
|
|
||||||
/// The [TextStyle] is not overridden during tick draw strategy decorateTicks.
|
|
||||||
/// If it is null, then the default is used.
|
|
||||||
class StaticTickProvider<D> extends TickProvider<D> {
|
|
||||||
final List<TickSpec<D>> tickSpec;
|
|
||||||
|
|
||||||
StaticTickProvider(this.tickSpec);
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Tick<D>> getTicks({
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required MutableScale<D> scale,
|
|
||||||
@required TickFormatter<D> formatter,
|
|
||||||
@required Map<D, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
@required AxisOrientation orientation,
|
|
||||||
bool viewportExtensionEnabled = false,
|
|
||||||
TickHint<D> tickHint,
|
|
||||||
}) {
|
|
||||||
final ticks = <Tick<D>>[];
|
|
||||||
|
|
||||||
bool allTicksHaveLabels = true;
|
|
||||||
|
|
||||||
for (TickSpec<D> spec in tickSpec) {
|
|
||||||
// When static ticks are being used with a numeric axis, extend the axis
|
|
||||||
// with the values specified.
|
|
||||||
if (scale is NumericScale || scale is DateTimeScale) {
|
|
||||||
scale.addDomain(spec.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save off whether all ticks have labels.
|
|
||||||
allTicksHaveLabels = allTicksHaveLabels && (spec.label != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the formatter's label if the tick spec does not provide one.
|
|
||||||
List<String> formattedValues;
|
|
||||||
if (allTicksHaveLabels == false) {
|
|
||||||
formattedValues = formatter.format(
|
|
||||||
tickSpec.map((spec) => spec.value).toList(), formatterValueCache,
|
|
||||||
stepSize: scale.domainStepSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < tickSpec.length; i++) {
|
|
||||||
final spec = tickSpec[i];
|
|
||||||
// We still check if the spec is within the viewport because we do not
|
|
||||||
// extend the axis for OrdinalScale.
|
|
||||||
if (scale.compareDomainValueToViewport(spec.value) == 0) {
|
|
||||||
final tick = Tick<D>(
|
|
||||||
value: spec.value,
|
|
||||||
textElement: graphicsFactory
|
|
||||||
.createTextElement(spec.label ?? formattedValues[i]),
|
|
||||||
locationPx: scale[spec.value]);
|
|
||||||
if (spec.style != null) {
|
|
||||||
tick.textElement.textStyle = graphicsFactory.createTextPaint()
|
|
||||||
..fontFamily = spec.style.fontFamily
|
|
||||||
..fontSize = spec.style.fontSize
|
|
||||||
..color = spec.style.color;
|
|
||||||
}
|
|
||||||
ticks.add(tick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow draw strategy to decorate the ticks.
|
|
||||||
tickDrawStrategy.decorateTicks(ticks);
|
|
||||||
|
|
||||||
return ticks;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) =>
|
|
||||||
other is StaticTickProvider && tickSpec == other.tickSpec;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => tickSpec.hashCode;
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
import '../../../common/text_element.dart';
|
|
||||||
|
|
||||||
/// A labeled point on an axis.
|
|
||||||
///
|
|
||||||
/// [D] is the type of the value this tick is associated with.
|
|
||||||
class Tick<D> {
|
|
||||||
/// The value that this tick represents
|
|
||||||
final D value;
|
|
||||||
|
|
||||||
/// [TextElement] for this tick.
|
|
||||||
TextElement textElement;
|
|
||||||
|
|
||||||
/// Location on the axis where this tick is rendered (in canvas coordinates).
|
|
||||||
double locationPx;
|
|
||||||
|
|
||||||
/// Offset of the label for this tick from its location.
|
|
||||||
///
|
|
||||||
/// This is a vertical offset for ticks on a vertical axis, or horizontal
|
|
||||||
/// offset for ticks on a horizontal axis.
|
|
||||||
double labelOffsetPx;
|
|
||||||
|
|
||||||
Tick(
|
|
||||||
{@required this.value,
|
|
||||||
@required this.textElement,
|
|
||||||
this.locationPx,
|
|
||||||
this.labelOffsetPx});
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'Tick(value: $value, locationPx: $locationPx, '
|
|
||||||
'labelOffsetPx: $labelOffsetPx)';
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import '../../common/datum_details.dart' show MeasureFormatter;
|
|
||||||
|
|
||||||
// TODO: Break out into separate files.
|
|
||||||
|
|
||||||
/// A strategy used for converting domain values of the ticks into Strings.
|
|
||||||
///
|
|
||||||
/// [D] is the domain type.
|
|
||||||
abstract class TickFormatter<D> {
|
|
||||||
const TickFormatter();
|
|
||||||
|
|
||||||
/// Formats a list of tick values.
|
|
||||||
List<String> format(List<D> tickValues, Map<D, String> cache, {num stepSize});
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class SimpleTickFormatterBase<D> implements TickFormatter<D> {
|
|
||||||
const SimpleTickFormatterBase();
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> format(List<D> tickValues, Map<D, String> cache,
|
|
||||||
{num stepSize}) =>
|
|
||||||
tickValues.map((value) {
|
|
||||||
// Try to use the cached formats first.
|
|
||||||
String formattedString = cache[value];
|
|
||||||
if (formattedString == null) {
|
|
||||||
formattedString = formatValue(value);
|
|
||||||
cache[value] = formattedString;
|
|
||||||
}
|
|
||||||
return formattedString;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
/// Formats a single tick value.
|
|
||||||
String formatValue(D value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A strategy that converts tick labels using toString().
|
|
||||||
class OrdinalTickFormatter extends SimpleTickFormatterBase<String> {
|
|
||||||
const OrdinalTickFormatter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
String formatValue(String value) => value;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) => other is OrdinalTickFormatter;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => 31;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A strategy for formatting the labels on numeric ticks using [NumberFormat].
|
|
||||||
///
|
|
||||||
/// The default format is [NumberFormat.decimalPattern].
|
|
||||||
class NumericTickFormatter extends SimpleTickFormatterBase<num> {
|
|
||||||
final MeasureFormatter formatter;
|
|
||||||
|
|
||||||
NumericTickFormatter._internal(this.formatter);
|
|
||||||
|
|
||||||
/// Construct a a new [NumericTickFormatter].
|
|
||||||
///
|
|
||||||
/// [formatter] optionally specify a formatter to be used. Defaults to using
|
|
||||||
/// [NumberFormat.decimalPattern] if none is specified.
|
|
||||||
factory NumericTickFormatter({MeasureFormatter formatter}) {
|
|
||||||
formatter ??= _getFormatter(NumberFormat.decimalPattern());
|
|
||||||
return NumericTickFormatter._internal(formatter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a new [NumericTickFormatter] that formats using [numberFormat].
|
|
||||||
factory NumericTickFormatter.fromNumberFormat(NumberFormat numberFormat) {
|
|
||||||
return NumericTickFormatter._internal(_getFormatter(numberFormat));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a new formatter that uses [NumberFormat.compactCurrency].
|
|
||||||
factory NumericTickFormatter.compactSimpleCurrency() {
|
|
||||||
return NumericTickFormatter._internal(
|
|
||||||
_getFormatter(NumberFormat.compactCurrency()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a [MeasureFormatter] that calls format on [numberFormat].
|
|
||||||
static MeasureFormatter _getFormatter(NumberFormat numberFormat) {
|
|
||||||
return (value) => numberFormat.format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String formatValue(num value) => formatter(value);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(other) =>
|
|
||||||
other is NumericTickFormatter && formatter == other.formatter;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => formatter.hashCode;
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../common/chart_context.dart' show ChartContext;
|
|
||||||
import 'axis.dart' show AxisOrientation;
|
|
||||||
import 'draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import 'scale.dart' show MutableScale;
|
|
||||||
import 'tick.dart' show Tick;
|
|
||||||
import 'tick_formatter.dart' show TickFormatter;
|
|
||||||
|
|
||||||
/// A strategy for selecting values for axis ticks based on the domain values.
|
|
||||||
///
|
|
||||||
/// [D] is the domain type.
|
|
||||||
abstract class TickProvider<D> {
|
|
||||||
/// Returns a list of ticks in value order that should be displayed.
|
|
||||||
///
|
|
||||||
/// This method should not return null. If no ticks are desired an empty list
|
|
||||||
/// should be returned.
|
|
||||||
///
|
|
||||||
/// [graphicsFactory] The graphics factory used for text measurement.
|
|
||||||
/// [scale] The scale of the data.
|
|
||||||
/// [formatter] The formatter to use for generating tick labels.
|
|
||||||
/// [orientation] Orientation of this axis ticks.
|
|
||||||
/// [tickDrawStrategy] Draw strategy for ticks.
|
|
||||||
/// [viewportExtensionEnabled] allow extending the viewport for 'niced' ticks.
|
|
||||||
/// [tickHint] tick values for provider to calculate a desired tick range.
|
|
||||||
List<Tick<D>> getTicks({
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required covariant MutableScale<D> scale,
|
|
||||||
@required TickFormatter<D> formatter,
|
|
||||||
@required Map<D, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
@required AxisOrientation orientation,
|
|
||||||
bool viewportExtensionEnabled = false,
|
|
||||||
TickHint<D> tickHint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A base tick provider.
|
|
||||||
abstract class BaseTickProvider<D> implements TickProvider<D> {
|
|
||||||
const BaseTickProvider();
|
|
||||||
|
|
||||||
/// Create ticks from [domainValues].
|
|
||||||
List<Tick<D>> createTicks(
|
|
||||||
List<D> domainValues, {
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required MutableScale<D> scale,
|
|
||||||
@required TickFormatter<D> formatter,
|
|
||||||
@required Map<D, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
num stepSize,
|
|
||||||
}) {
|
|
||||||
final ticks = <Tick<D>>[];
|
|
||||||
final labels =
|
|
||||||
formatter.format(domainValues, formatterValueCache, stepSize: stepSize);
|
|
||||||
|
|
||||||
for (var i = 0; i < domainValues.length; i++) {
|
|
||||||
final value = domainValues[i];
|
|
||||||
final tick = Tick(
|
|
||||||
value: value,
|
|
||||||
textElement: graphicsFactory.createTextElement(labels[i]),
|
|
||||||
locationPx: scale[value]);
|
|
||||||
|
|
||||||
ticks.add(tick);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow draw strategy to decorate the ticks.
|
|
||||||
tickDrawStrategy.decorateTicks(ticks);
|
|
||||||
|
|
||||||
return ticks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A hint for the tick provider to determine step size and tick count.
|
|
||||||
class TickHint<D> {
|
|
||||||
/// The starting hint tick value.
|
|
||||||
final D start;
|
|
||||||
|
|
||||||
/// The ending hint tick value.
|
|
||||||
final D end;
|
|
||||||
|
|
||||||
/// Number of ticks.
|
|
||||||
final int tickCount;
|
|
||||||
|
|
||||||
TickHint(this.start, this.end, {this.tickCount});
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../axis.dart' show AxisOrientation;
|
|
||||||
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import '../tick.dart' show Tick;
|
|
||||||
import '../tick_formatter.dart' show TickFormatter;
|
|
||||||
import '../tick_provider.dart' show TickProvider, TickHint;
|
|
||||||
import 'date_time_scale.dart' show DateTimeScale;
|
|
||||||
import 'day_time_stepper.dart' show DayTimeStepper;
|
|
||||||
import 'hour_time_stepper.dart' show HourTimeStepper;
|
|
||||||
import 'minute_time_stepper.dart' show MinuteTimeStepper;
|
|
||||||
import 'month_time_stepper.dart' show MonthTimeStepper;
|
|
||||||
import 'time_range_tick_provider.dart' show TimeRangeTickProvider;
|
|
||||||
import 'time_range_tick_provider_impl.dart' show TimeRangeTickProviderImpl;
|
|
||||||
import 'year_time_stepper.dart' show YearTimeStepper;
|
|
||||||
|
|
||||||
/// Tick provider for date and time.
|
|
||||||
///
|
|
||||||
/// When determining the ticks for a given domain, the provider will use choose
|
|
||||||
/// one of the internal tick providers appropriate to the size of the data's
|
|
||||||
/// domain range. It does this in an attempt to ensure there are at least 3
|
|
||||||
/// ticks, before jumping to the next more fine grain provider. The 3 tick
|
|
||||||
/// minimum is not a hard rule as some of the ticks might be eliminated because
|
|
||||||
/// of collisions, but the data was within the targeted range.
|
|
||||||
///
|
|
||||||
/// Once a tick provider is chosen the selection of ticks is done by the child
|
|
||||||
/// tick provider.
|
|
||||||
class AutoAdjustingDateTimeTickProvider implements TickProvider<DateTime> {
|
|
||||||
/// List of tick providers to be selected from.
|
|
||||||
final List<TimeRangeTickProvider> _potentialTickProviders;
|
|
||||||
|
|
||||||
AutoAdjustingDateTimeTickProvider._internal(
|
|
||||||
List<TimeRangeTickProvider> tickProviders)
|
|
||||||
: _potentialTickProviders = tickProviders;
|
|
||||||
|
|
||||||
/// Creates a default [AutoAdjustingDateTimeTickProvider] for day and time.
|
|
||||||
factory AutoAdjustingDateTimeTickProvider.createDefault(
|
|
||||||
DateTimeFactory dateTimeFactory) {
|
|
||||||
return AutoAdjustingDateTimeTickProvider._internal([
|
|
||||||
createYearTickProvider(dateTimeFactory),
|
|
||||||
createMonthTickProvider(dateTimeFactory),
|
|
||||||
createDayTickProvider(dateTimeFactory),
|
|
||||||
createHourTickProvider(dateTimeFactory),
|
|
||||||
createMinuteTickProvider(dateTimeFactory)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a default [AutoAdjustingDateTimeTickProvider] for day only.
|
|
||||||
factory AutoAdjustingDateTimeTickProvider.createWithoutTime(
|
|
||||||
DateTimeFactory dateTimeFactory) {
|
|
||||||
return AutoAdjustingDateTimeTickProvider._internal([
|
|
||||||
createYearTickProvider(dateTimeFactory),
|
|
||||||
createMonthTickProvider(dateTimeFactory),
|
|
||||||
createDayTickProvider(dateTimeFactory)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates [AutoAdjustingDateTimeTickProvider] with custom tick providers.
|
|
||||||
///
|
|
||||||
/// [potentialTickProviders] must have at least one [TimeRangeTickProvider]
|
|
||||||
/// and this list of tick providers are used in the order they are provided.
|
|
||||||
factory AutoAdjustingDateTimeTickProvider.createWith(
|
|
||||||
List<TimeRangeTickProvider> potentialTickProviders) {
|
|
||||||
if (potentialTickProviders == null || potentialTickProviders.isEmpty) {
|
|
||||||
throw ArgumentError('At least one TimeRangeTickProvider is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
return AutoAdjustingDateTimeTickProvider._internal(potentialTickProviders);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generates a list of ticks for the given data which should not collide
|
|
||||||
/// unless the range is not large enough.
|
|
||||||
@override
|
|
||||||
List<Tick<DateTime>> getTicks({
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required DateTimeScale scale,
|
|
||||||
@required TickFormatter<DateTime> formatter,
|
|
||||||
@required Map<DateTime, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
@required AxisOrientation orientation,
|
|
||||||
bool viewportExtensionEnabled = false,
|
|
||||||
TickHint<DateTime> tickHint,
|
|
||||||
}) {
|
|
||||||
List<TimeRangeTickProvider> tickProviders;
|
|
||||||
|
|
||||||
/// If tick hint is provided, use the closest tick provider, otherwise
|
|
||||||
/// look through the tick providers for one that provides sufficient ticks
|
|
||||||
/// for the viewport.
|
|
||||||
if (tickHint != null) {
|
|
||||||
tickProviders = [_getClosestTickProvider(tickHint)];
|
|
||||||
} else {
|
|
||||||
tickProviders = _potentialTickProviders;
|
|
||||||
}
|
|
||||||
|
|
||||||
final lastTickProvider = tickProviders.last;
|
|
||||||
|
|
||||||
final viewport = scale.viewportDomain;
|
|
||||||
for (final tickProvider in tickProviders) {
|
|
||||||
final isLastProvider = (tickProvider == lastTickProvider);
|
|
||||||
if (isLastProvider ||
|
|
||||||
tickProvider.providesSufficientTicksForRange(viewport)) {
|
|
||||||
return tickProvider.getTicks(
|
|
||||||
context: context,
|
|
||||||
graphicsFactory: graphicsFactory,
|
|
||||||
scale: scale,
|
|
||||||
formatter: formatter,
|
|
||||||
formatterValueCache: formatterValueCache,
|
|
||||||
tickDrawStrategy: tickDrawStrategy,
|
|
||||||
orientation: orientation,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Tick<DateTime>>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the closest tick provider based on the tick hint.
|
|
||||||
TimeRangeTickProvider _getClosestTickProvider(TickHint<DateTime> tickHint) {
|
|
||||||
final stepSize = ((tickHint.end.difference(tickHint.start).inMilliseconds) /
|
|
||||||
(tickHint.tickCount - 1))
|
|
||||||
.round();
|
|
||||||
|
|
||||||
int minDifference;
|
|
||||||
TimeRangeTickProvider closestTickProvider;
|
|
||||||
|
|
||||||
for (final tickProvider in _potentialTickProviders) {
|
|
||||||
final difference =
|
|
||||||
(stepSize - tickProvider.getClosestStepSize(stepSize)).abs();
|
|
||||||
if (minDifference == null || minDifference > difference) {
|
|
||||||
minDifference = difference;
|
|
||||||
closestTickProvider = tickProvider;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return closestTickProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
static TimeRangeTickProvider createYearTickProvider(
|
|
||||||
DateTimeFactory dateTimeFactory) =>
|
|
||||||
TimeRangeTickProviderImpl(YearTimeStepper(dateTimeFactory));
|
|
||||||
|
|
||||||
static TimeRangeTickProvider createMonthTickProvider(
|
|
||||||
DateTimeFactory dateTimeFactory) =>
|
|
||||||
TimeRangeTickProviderImpl(MonthTimeStepper(dateTimeFactory));
|
|
||||||
|
|
||||||
static TimeRangeTickProvider createDayTickProvider(
|
|
||||||
DateTimeFactory dateTimeFactory) =>
|
|
||||||
TimeRangeTickProviderImpl(DayTimeStepper(dateTimeFactory));
|
|
||||||
|
|
||||||
static TimeRangeTickProvider createHourTickProvider(
|
|
||||||
DateTimeFactory dateTimeFactory) =>
|
|
||||||
TimeRangeTickProviderImpl(HourTimeStepper(dateTimeFactory));
|
|
||||||
|
|
||||||
static TimeRangeTickProvider createMinuteTickProvider(
|
|
||||||
DateTimeFactory dateTimeFactory) =>
|
|
||||||
TimeRangeTickProviderImpl(MinuteTimeStepper(dateTimeFactory));
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart';
|
|
||||||
import 'date_time_extents.dart' show DateTimeExtents;
|
|
||||||
import 'time_stepper.dart'
|
|
||||||
show TimeStepper, TimeStepIteratorFactory, TimeStepIterator;
|
|
||||||
|
|
||||||
/// A base stepper for operating with DateTimeFactory and time range steps.
|
|
||||||
abstract class BaseTimeStepper implements TimeStepper {
|
|
||||||
/// The factory to generate a DateTime object.
|
|
||||||
///
|
|
||||||
/// This is needed because Dart's DateTime does not handle time zone.
|
|
||||||
/// There is a time zone aware library that we could use that implements the
|
|
||||||
/// DateTime interface.
|
|
||||||
final DateTimeFactory dateTimeFactory;
|
|
||||||
|
|
||||||
_TimeStepIteratorFactoryImpl _stepsIterable;
|
|
||||||
|
|
||||||
BaseTimeStepper(this.dateTimeFactory);
|
|
||||||
|
|
||||||
/// Get the step time before or on the given [time] from [tickIncrement].
|
|
||||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement);
|
|
||||||
|
|
||||||
/// Get the next step time after [time] from [tickIncrement].
|
|
||||||
DateTime getNextStepTime(DateTime time, int tickIncrement);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int getStepCountBetween(DateTimeExtents timeExtent, int tickIncrement) {
|
|
||||||
checkTickIncrement(tickIncrement);
|
|
||||||
final min = timeExtent.start;
|
|
||||||
final max = timeExtent.end;
|
|
||||||
var time = getStepTimeAfterInclusive(min, tickIncrement);
|
|
||||||
|
|
||||||
var cnt = 0;
|
|
||||||
while (time.compareTo(max) <= 0) {
|
|
||||||
cnt++;
|
|
||||||
time = getNextStepTime(time, tickIncrement);
|
|
||||||
}
|
|
||||||
return cnt;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
TimeStepIteratorFactory getSteps(DateTimeExtents timeExtent) {
|
|
||||||
// Keep the steps iterable unless time extent changes, so the same iterator
|
|
||||||
// can be used and reset for different increments.
|
|
||||||
if (_stepsIterable == null || _stepsIterable.timeExtent != timeExtent) {
|
|
||||||
_stepsIterable = _TimeStepIteratorFactoryImpl(timeExtent, this);
|
|
||||||
}
|
|
||||||
return _stepsIterable;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTimeExtents updateBoundingSteps(DateTimeExtents timeExtent) {
|
|
||||||
final stepBefore = getStepTimeBeforeInclusive(timeExtent.start, 1);
|
|
||||||
final stepAfter = getStepTimeAfterInclusive(timeExtent.end, 1);
|
|
||||||
|
|
||||||
return DateTimeExtents(start: stepBefore, end: stepAfter);
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime getStepTimeAfterInclusive(DateTime time, int tickIncrement) {
|
|
||||||
final boundedStart = getStepTimeBeforeInclusive(time, tickIncrement);
|
|
||||||
if (boundedStart == time) {
|
|
||||||
return boundedStart;
|
|
||||||
}
|
|
||||||
return getNextStepTime(boundedStart, tickIncrement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimeStepIteratorImpl implements TimeStepIterator {
|
|
||||||
final DateTime extentStartTime;
|
|
||||||
final DateTime extentEndTime;
|
|
||||||
final BaseTimeStepper stepper;
|
|
||||||
DateTime _current;
|
|
||||||
int _tickIncrement = 1;
|
|
||||||
|
|
||||||
_TimeStepIteratorImpl(
|
|
||||||
this.extentStartTime, this.extentEndTime, this.stepper) {
|
|
||||||
reset(_tickIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool moveNext() {
|
|
||||||
if (_current == null) {
|
|
||||||
_current =
|
|
||||||
stepper.getStepTimeAfterInclusive(extentStartTime, _tickIncrement);
|
|
||||||
} else {
|
|
||||||
_current = stepper.getNextStepTime(_current, _tickIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _current.compareTo(extentEndTime) <= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTime get current => _current;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TimeStepIterator reset(int tickIncrement) {
|
|
||||||
checkTickIncrement(tickIncrement);
|
|
||||||
_tickIncrement = tickIncrement;
|
|
||||||
_current = null;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TimeStepIteratorFactoryImpl extends TimeStepIteratorFactory {
|
|
||||||
final DateTimeExtents timeExtent;
|
|
||||||
final _TimeStepIteratorImpl _timeStepIterator;
|
|
||||||
|
|
||||||
_TimeStepIteratorFactoryImpl._internal(
|
|
||||||
_TimeStepIteratorImpl timeStepIterator, this.timeExtent)
|
|
||||||
: _timeStepIterator = timeStepIterator;
|
|
||||||
|
|
||||||
factory _TimeStepIteratorFactoryImpl(
|
|
||||||
DateTimeExtents timeExtent, BaseTimeStepper stepper) {
|
|
||||||
final startTime = timeExtent.start;
|
|
||||||
final endTime = timeExtent.end;
|
|
||||||
return _TimeStepIteratorFactoryImpl._internal(
|
|
||||||
_TimeStepIteratorImpl(startTime, endTime, stepper), timeExtent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
TimeStepIterator get iterator => _timeStepIterator;
|
|
||||||
}
|
|
||||||
|
|
||||||
void checkTickIncrement(int tickIncrement) {
|
|
||||||
/// tickIncrement must be greater than 0
|
|
||||||
assert(tickIncrement > 0);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import '../axis.dart' show Axis;
|
|
||||||
import '../tick_formatter.dart' show TickFormatter;
|
|
||||||
import '../tick_provider.dart' show TickProvider;
|
|
||||||
import 'auto_adjusting_date_time_tick_provider.dart'
|
|
||||||
show AutoAdjustingDateTimeTickProvider;
|
|
||||||
import 'date_time_extents.dart' show DateTimeExtents;
|
|
||||||
import 'date_time_scale.dart' show DateTimeScale;
|
|
||||||
import 'date_time_tick_formatter.dart' show DateTimeTickFormatter;
|
|
||||||
|
|
||||||
class DateTimeAxis extends Axis<DateTime> {
|
|
||||||
DateTimeAxis(DateTimeFactory dateTimeFactory,
|
|
||||||
{TickProvider tickProvider, TickFormatter tickFormatter})
|
|
||||||
: super(
|
|
||||||
tickProvider: tickProvider ??
|
|
||||||
AutoAdjustingDateTimeTickProvider.createDefault(dateTimeFactory),
|
|
||||||
tickFormatter:
|
|
||||||
tickFormatter ?? DateTimeTickFormatter(dateTimeFactory),
|
|
||||||
scale: DateTimeScale(dateTimeFactory),
|
|
||||||
);
|
|
||||||
|
|
||||||
void setScaleViewport(DateTimeExtents viewport) {
|
|
||||||
(mutableScale as DateTimeScale).viewportDomain = viewport;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import '../linear/linear_scale.dart' show LinearScale;
|
|
||||||
import '../numeric_extents.dart' show NumericExtents;
|
|
||||||
import '../scale.dart'
|
|
||||||
show MutableScale, StepSizeConfig, RangeBandConfig, ScaleOutputExtent;
|
|
||||||
import 'date_time_extents.dart' show DateTimeExtents;
|
|
||||||
|
|
||||||
/// [DateTimeScale] is a wrapper for [LinearScale].
|
|
||||||
/// [DateTime] values are converted to millisecondsSinceEpoch and passed to the
|
|
||||||
/// [LinearScale].
|
|
||||||
class DateTimeScale extends MutableScale<DateTime> {
|
|
||||||
final DateTimeFactory dateTimeFactory;
|
|
||||||
final LinearScale _linearScale;
|
|
||||||
|
|
||||||
DateTimeScale(this.dateTimeFactory) : _linearScale = LinearScale();
|
|
||||||
|
|
||||||
DateTimeScale._copy(DateTimeScale other)
|
|
||||||
: dateTimeFactory = other.dateTimeFactory,
|
|
||||||
_linearScale = other._linearScale.copy();
|
|
||||||
|
|
||||||
@override
|
|
||||||
num operator [](DateTime domainValue) =>
|
|
||||||
_linearScale[domainValue.millisecondsSinceEpoch];
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTime reverse(double pixelLocation) =>
|
|
||||||
dateTimeFactory.createDateTimeFromMilliSecondsSinceEpoch(
|
|
||||||
_linearScale.reverse(pixelLocation).round());
|
|
||||||
|
|
||||||
@override
|
|
||||||
void resetDomain() {
|
|
||||||
_linearScale.resetDomain();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
set stepSizeConfig(StepSizeConfig config) {
|
|
||||||
_linearScale.stepSizeConfig = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
StepSizeConfig get stepSizeConfig => _linearScale.stepSizeConfig;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set rangeBandConfig(RangeBandConfig barGroupWidthConfig) {
|
|
||||||
_linearScale.rangeBandConfig = barGroupWidthConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void setViewportSettings(double viewportScale, double viewportTranslatePx) {
|
|
||||||
_linearScale.setViewportSettings(viewportScale, viewportTranslatePx);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
set range(ScaleOutputExtent extent) {
|
|
||||||
_linearScale.range = extent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void addDomain(DateTime domainValue) {
|
|
||||||
_linearScale.addDomain(domainValue.millisecondsSinceEpoch);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void resetViewportSettings() {
|
|
||||||
_linearScale.resetViewportSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTimeExtents get viewportDomain {
|
|
||||||
final extents = _linearScale.viewportDomain;
|
|
||||||
return DateTimeExtents(
|
|
||||||
start: dateTimeFactory
|
|
||||||
.createDateTimeFromMilliSecondsSinceEpoch(extents.min.toInt()),
|
|
||||||
end: dateTimeFactory
|
|
||||||
.createDateTimeFromMilliSecondsSinceEpoch(extents.max.toInt()));
|
|
||||||
}
|
|
||||||
|
|
||||||
set viewportDomain(DateTimeExtents extents) {
|
|
||||||
_linearScale.viewportDomain = NumericExtents(
|
|
||||||
extents.start.millisecondsSinceEpoch,
|
|
||||||
extents.end.millisecondsSinceEpoch);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTimeScale copy() => DateTimeScale._copy(this);
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get viewportTranslatePx => _linearScale.viewportTranslatePx;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get viewportScalingFactor => _linearScale.viewportScalingFactor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isRangeValueWithinViewport(double rangeValue) =>
|
|
||||||
_linearScale.isRangeValueWithinViewport(rangeValue);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int compareDomainValueToViewport(DateTime domainValue) => _linearScale
|
|
||||||
.compareDomainValueToViewport(domainValue.millisecondsSinceEpoch);
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get rangeBand => _linearScale.rangeBand;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get stepSize => _linearScale.stepSize;
|
|
||||||
|
|
||||||
@override
|
|
||||||
double get domainStepSize => _linearScale.domainStepSize;
|
|
||||||
|
|
||||||
@override
|
|
||||||
RangeBandConfig get rangeBandConfig => _linearScale.rangeBandConfig;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get rangeWidth => _linearScale.rangeWidth;
|
|
||||||
|
|
||||||
@override
|
|
||||||
ScaleOutputExtent get range => _linearScale.range;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool canTranslate(DateTime domainValue) =>
|
|
||||||
_linearScale.canTranslate(domainValue.millisecondsSinceEpoch);
|
|
||||||
|
|
||||||
NumericExtents get dataExtent => _linearScale.dataExtent;
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import '../tick_formatter.dart' show TickFormatter;
|
|
||||||
import 'hour_tick_formatter.dart' show HourTickFormatter;
|
|
||||||
import 'time_tick_formatter.dart' show TimeTickFormatter;
|
|
||||||
import 'time_tick_formatter_impl.dart'
|
|
||||||
show CalendarField, TimeTickFormatterImpl;
|
|
||||||
|
|
||||||
/// A [TickFormatter] that formats date/time values based on minimum difference
|
|
||||||
/// between subsequent ticks.
|
|
||||||
///
|
|
||||||
/// This formatter assumes that the Tick values passed in are sorted in
|
|
||||||
/// increasing order.
|
|
||||||
///
|
|
||||||
/// This class is setup with a list of formatters that format the input ticks at
|
|
||||||
/// a given time resolution. The time resolution which will accurately display
|
|
||||||
/// the difference between 2 subsequent ticks is picked. Each time resolution
|
|
||||||
/// can be setup with a [TimeTickFormatter], which is used to format ticks as
|
|
||||||
/// regular or transition ticks based on whether the tick has crossed the time
|
|
||||||
/// boundary defined in the [TimeTickFormatter].
|
|
||||||
class DateTimeTickFormatter implements TickFormatter<DateTime> {
|
|
||||||
static const int SECOND = 1000;
|
|
||||||
static const int MINUTE = 60 * SECOND;
|
|
||||||
static const int HOUR = 60 * MINUTE;
|
|
||||||
static const int DAY = 24 * HOUR;
|
|
||||||
|
|
||||||
/// Used for the case when there is only one formatter.
|
|
||||||
static const int ANY = -1;
|
|
||||||
|
|
||||||
final Map<int, TimeTickFormatter> _timeFormatters;
|
|
||||||
|
|
||||||
/// Creates a [DateTimeTickFormatter] that works well with time tick provider
|
|
||||||
/// classes.
|
|
||||||
///
|
|
||||||
/// The default formatter makes assumptions on border cases that time tick
|
|
||||||
/// providers will still provide ticks that make sense. Example: Tick provider
|
|
||||||
/// does not provide ticks with 23 hour intervals. For custom tick providers
|
|
||||||
/// where these assumptions are not correct, please create a custom
|
|
||||||
/// [TickFormatter].
|
|
||||||
factory DateTimeTickFormatter(DateTimeFactory dateTimeFactory,
|
|
||||||
{Map<int, TimeTickFormatter> overrides}) {
|
|
||||||
final Map<int, TimeTickFormatter> map = {
|
|
||||||
MINUTE: TimeTickFormatterImpl(
|
|
||||||
dateTimeFactory: dateTimeFactory,
|
|
||||||
simpleFormat: 'mm',
|
|
||||||
transitionFormat: 'h mm',
|
|
||||||
transitionField: CalendarField.hourOfDay),
|
|
||||||
HOUR: HourTickFormatter(
|
|
||||||
dateTimeFactory: dateTimeFactory,
|
|
||||||
simpleFormat: 'h',
|
|
||||||
transitionFormat: 'MMM d ha',
|
|
||||||
noonFormat: 'ha'),
|
|
||||||
23 * HOUR: TimeTickFormatterImpl(
|
|
||||||
dateTimeFactory: dateTimeFactory,
|
|
||||||
simpleFormat: 'd',
|
|
||||||
transitionFormat: 'MMM d',
|
|
||||||
transitionField: CalendarField.month),
|
|
||||||
28 * DAY: TimeTickFormatterImpl(
|
|
||||||
dateTimeFactory: dateTimeFactory,
|
|
||||||
simpleFormat: 'MMM',
|
|
||||||
transitionFormat: 'MMM yyyy',
|
|
||||||
transitionField: CalendarField.year),
|
|
||||||
364 * DAY: TimeTickFormatterImpl(
|
|
||||||
dateTimeFactory: dateTimeFactory,
|
|
||||||
simpleFormat: 'yyyy',
|
|
||||||
transitionFormat: 'yyyy',
|
|
||||||
transitionField: CalendarField.year),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Allow the user to override some of the defaults.
|
|
||||||
if (overrides != null) {
|
|
||||||
map.addAll(overrides);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTimeTickFormatter._internal(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [DateTimeTickFormatter] without the time component.
|
|
||||||
factory DateTimeTickFormatter.withoutTime(DateTimeFactory dateTimeFactory) {
|
|
||||||
return DateTimeTickFormatter._internal({
|
|
||||||
23 * HOUR: TimeTickFormatterImpl(
|
|
||||||
dateTimeFactory: dateTimeFactory,
|
|
||||||
simpleFormat: 'd',
|
|
||||||
transitionFormat: 'MMM d',
|
|
||||||
transitionField: CalendarField.month),
|
|
||||||
28 * DAY: TimeTickFormatterImpl(
|
|
||||||
dateTimeFactory: dateTimeFactory,
|
|
||||||
simpleFormat: 'MMM',
|
|
||||||
transitionFormat: 'MMM yyyy',
|
|
||||||
transitionField: CalendarField.year),
|
|
||||||
365 * DAY: TimeTickFormatterImpl(
|
|
||||||
dateTimeFactory: dateTimeFactory,
|
|
||||||
simpleFormat: 'yyyy',
|
|
||||||
transitionFormat: 'yyyy',
|
|
||||||
transitionField: CalendarField.year),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [DateTimeTickFormatter] that formats all ticks the same.
|
|
||||||
///
|
|
||||||
/// Only use this formatter for data with fixed intervals, otherwise use the
|
|
||||||
/// default, or build from scratch.
|
|
||||||
///
|
|
||||||
/// [formatter] The format for all ticks.
|
|
||||||
factory DateTimeTickFormatter.uniform(TimeTickFormatter formatter) {
|
|
||||||
return DateTimeTickFormatter._internal({ANY: formatter});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a [DateTimeTickFormatter] that formats ticks with [formatters].
|
|
||||||
///
|
|
||||||
/// The formatters are expected to be provided with keys in increasing order.
|
|
||||||
factory DateTimeTickFormatter.withFormatters(
|
|
||||||
Map<int, TimeTickFormatter> formatters) {
|
|
||||||
// Formatters must be non empty.
|
|
||||||
if (formatters == null || formatters.isEmpty) {
|
|
||||||
throw ArgumentError('At least one TimeTickFormatter is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTimeTickFormatter._internal(formatters);
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTimeTickFormatter._internal(this._timeFormatters) {
|
|
||||||
// If there is only one formatter, just use this one and skip this check.
|
|
||||||
if (_timeFormatters.length == 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_checkPositiveAndSorted(_timeFormatters.keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<String> format(List<DateTime> tickValues, Map<DateTime, String> cache,
|
|
||||||
{@required num stepSize}) {
|
|
||||||
final tickLabels = <String>[];
|
|
||||||
if (tickValues.isEmpty) {
|
|
||||||
return tickLabels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the formatter that is the largest interval that has enough
|
|
||||||
// resolution to describe the difference between ticks. If no such formatter
|
|
||||||
// exists pick the highest res one.
|
|
||||||
var formatter = _timeFormatters[_timeFormatters.keys.first];
|
|
||||||
var formatterFound = false;
|
|
||||||
if (_timeFormatters.keys.first == ANY) {
|
|
||||||
formatterFound = true;
|
|
||||||
} else {
|
|
||||||
int minTimeBetweenTicks = stepSize.toInt();
|
|
||||||
|
|
||||||
// TODO: Skip the formatter if the formatter's step size is
|
|
||||||
// smaller than the minimum step size of the data.
|
|
||||||
|
|
||||||
var keys = _timeFormatters.keys.iterator;
|
|
||||||
while (keys.moveNext() && !formatterFound) {
|
|
||||||
if (keys.current > minTimeBetweenTicks) {
|
|
||||||
formatterFound = true;
|
|
||||||
} else {
|
|
||||||
formatter = _timeFormatters[keys.current];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format the ticks.
|
|
||||||
final tickValuesIt = tickValues.iterator;
|
|
||||||
|
|
||||||
var tickValue = (tickValuesIt..moveNext()).current;
|
|
||||||
var prevTickValue = tickValue;
|
|
||||||
tickLabels.add(formatter.formatFirstTick(tickValue));
|
|
||||||
|
|
||||||
while (tickValuesIt.moveNext()) {
|
|
||||||
tickValue = tickValuesIt.current;
|
|
||||||
if (formatter.isTransition(tickValue, prevTickValue)) {
|
|
||||||
tickLabels.add(formatter.formatTransitionTick(tickValue));
|
|
||||||
} else {
|
|
||||||
tickLabels.add(formatter.formatSimpleTick(tickValue));
|
|
||||||
}
|
|
||||||
prevTickValue = tickValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return tickLabels;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _checkPositiveAndSorted(Iterable<int> values) {
|
|
||||||
final valuesIterator = values.iterator;
|
|
||||||
var prev = (valuesIterator..moveNext()).current;
|
|
||||||
var isSorted = true;
|
|
||||||
|
|
||||||
// Only need to check the first value, because the values after are expected
|
|
||||||
// to be greater.
|
|
||||||
if (prev <= 0) {
|
|
||||||
throw ArgumentError('Formatter keys must be positive');
|
|
||||||
}
|
|
||||||
|
|
||||||
while (valuesIterator.moveNext() && isSorted) {
|
|
||||||
isSorted = prev < valuesIterator.current;
|
|
||||||
prev = valuesIterator.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSorted) {
|
|
||||||
throw ArgumentError(
|
|
||||||
'Formatters must be sorted with keys in increasing order');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import 'base_time_stepper.dart' show BaseTimeStepper;
|
|
||||||
|
|
||||||
/// Day stepper.
|
|
||||||
class DayTimeStepper extends BaseTimeStepper {
|
|
||||||
// TODO: Remove the 14 day increment if we add week stepper.
|
|
||||||
static const _defaultIncrements = [1, 2, 3, 7, 14];
|
|
||||||
static const _hoursInDay = 24;
|
|
||||||
|
|
||||||
final List<int> _allowedTickIncrements;
|
|
||||||
|
|
||||||
DayTimeStepper._internal(
|
|
||||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
|
||||||
: _allowedTickIncrements = increments,
|
|
||||||
super(dateTimeFactory);
|
|
||||||
|
|
||||||
factory DayTimeStepper(DateTimeFactory dateTimeFactory,
|
|
||||||
{List<int> allowedTickIncrements}) {
|
|
||||||
// Set the default increments if null.
|
|
||||||
allowedTickIncrements ??= _defaultIncrements;
|
|
||||||
|
|
||||||
// Must have at least one increment option.
|
|
||||||
assert(allowedTickIncrements.isNotEmpty);
|
|
||||||
// All increments must be > 0.
|
|
||||||
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
|
|
||||||
|
|
||||||
return DayTimeStepper._internal(dateTimeFactory, allowedTickIncrements);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get typicalStepSizeMs => _hoursInDay * 3600 * 1000;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
|
||||||
|
|
||||||
/// Get the step time before or on the given [time] from [tickIncrement].
|
|
||||||
///
|
|
||||||
/// Increments are based off the beginning of the month.
|
|
||||||
/// Ex. 5 day increments in a month is 1,6,11,16,21,26,31
|
|
||||||
/// Ex. Time is Aug 20, increment is 1 day. Returns Aug 20.
|
|
||||||
/// Ex. Time is Aug 20, increment is 2 days. Returns Aug 19 because 2 day
|
|
||||||
/// increments in a month is 1,3,5,7,9,11,13,15,17,19,21....
|
|
||||||
@override
|
|
||||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
|
||||||
final dayRemainder = (time.day - 1) % tickIncrement;
|
|
||||||
// Subtract an extra hour in case stepping through a daylight saving change.
|
|
||||||
final dayBefore = dayRemainder > 0
|
|
||||||
? time.subtract(Duration(hours: (_hoursInDay * dayRemainder) - 1))
|
|
||||||
: time;
|
|
||||||
// Explicitly leaving off hours and beyond to truncate to start of day.
|
|
||||||
final stepBefore = dateTimeFactory.createDateTime(
|
|
||||||
dayBefore.year, dayBefore.month, dayBefore.day);
|
|
||||||
|
|
||||||
return stepBefore;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
|
||||||
// Add an extra hour in case stepping through a daylight saving change.
|
|
||||||
final stepAfter =
|
|
||||||
time.add(Duration(hours: (_hoursInDay * tickIncrement) + 1));
|
|
||||||
// Explicitly leaving off hours and beyond to truncate to start of day.
|
|
||||||
return dateTimeFactory.createDateTime(
|
|
||||||
stepAfter.year, stepAfter.month, stepAfter.day);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:intl/intl.dart' show DateFormat;
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
import '../../../../common/date_time_factory.dart';
|
|
||||||
import 'time_tick_formatter_impl.dart'
|
|
||||||
show CalendarField, TimeTickFormatterImpl;
|
|
||||||
|
|
||||||
/// Hour specific tick formatter which will format noon differently.
|
|
||||||
class HourTickFormatter extends TimeTickFormatterImpl {
|
|
||||||
DateFormat _noonFormat;
|
|
||||||
|
|
||||||
HourTickFormatter(
|
|
||||||
{@required DateTimeFactory dateTimeFactory,
|
|
||||||
@required String simpleFormat,
|
|
||||||
@required String transitionFormat,
|
|
||||||
@required String noonFormat})
|
|
||||||
: super(
|
|
||||||
dateTimeFactory: dateTimeFactory,
|
|
||||||
simpleFormat: simpleFormat,
|
|
||||||
transitionFormat: transitionFormat,
|
|
||||||
transitionField: CalendarField.date) {
|
|
||||||
_noonFormat = dateTimeFactory.createDateFormat(noonFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String formatSimpleTick(DateTime date) {
|
|
||||||
return (date.hour == 12)
|
|
||||||
? _noonFormat.format(date)
|
|
||||||
: super.formatSimpleTick(date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import 'base_time_stepper.dart' show BaseTimeStepper;
|
|
||||||
|
|
||||||
/// Hour stepper.
|
|
||||||
class HourTimeStepper extends BaseTimeStepper {
|
|
||||||
static const _defaultIncrements = [1, 2, 3, 4, 6, 12, 24];
|
|
||||||
static const _hoursInDay = 24;
|
|
||||||
static const _millisecondsInHour = 3600 * 1000;
|
|
||||||
|
|
||||||
final List<int> _allowedTickIncrements;
|
|
||||||
|
|
||||||
HourTimeStepper._internal(
|
|
||||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
|
||||||
: _allowedTickIncrements = increments,
|
|
||||||
super(dateTimeFactory);
|
|
||||||
|
|
||||||
factory HourTimeStepper(DateTimeFactory dateTimeFactory,
|
|
||||||
{List<int> allowedTickIncrements}) {
|
|
||||||
// Set the default increments if null.
|
|
||||||
allowedTickIncrements ??= _defaultIncrements;
|
|
||||||
|
|
||||||
// Must have at least one increment option.
|
|
||||||
assert(allowedTickIncrements.isNotEmpty);
|
|
||||||
// All increments must be between 1 and 24 inclusive.
|
|
||||||
assert(allowedTickIncrements
|
|
||||||
.any((increment) => increment <= 0 || increment > 24) ==
|
|
||||||
false);
|
|
||||||
|
|
||||||
return HourTimeStepper._internal(dateTimeFactory, allowedTickIncrements);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get typicalStepSizeMs => _millisecondsInHour;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
|
||||||
|
|
||||||
/// Get the step time before or on the given [time] from [tickIncrement].
|
|
||||||
///
|
|
||||||
/// Guarantee a step at the start of the next day.
|
|
||||||
/// Ex. Time is Aug 20 10 AM, increment is 1 hour. Returns 10 AM.
|
|
||||||
/// Ex. Time is Aug 20 6 AM, increment is 4 hours. Returns 4 AM.
|
|
||||||
@override
|
|
||||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
|
||||||
final nextDay = dateTimeFactory
|
|
||||||
.createDateTime(time.year, time.month, time.day)
|
|
||||||
.add(Duration(hours: _hoursInDay + 1));
|
|
||||||
final nextDayStart = dateTimeFactory.createDateTime(
|
|
||||||
nextDay.year, nextDay.month, nextDay.day);
|
|
||||||
|
|
||||||
final hoursToNextDay =
|
|
||||||
((nextDayStart.millisecondsSinceEpoch - time.millisecondsSinceEpoch) /
|
|
||||||
_millisecondsInHour)
|
|
||||||
.ceil();
|
|
||||||
|
|
||||||
final hoursRemainder = hoursToNextDay % tickIncrement;
|
|
||||||
final rewindHours =
|
|
||||||
hoursRemainder == 0 ? 0 : tickIncrement - hoursRemainder;
|
|
||||||
final stepBefore = dateTimeFactory.createDateTime(
|
|
||||||
time.year, time.month, time.day, time.hour - rewindHours);
|
|
||||||
|
|
||||||
return stepBefore;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get next step time.
|
|
||||||
///
|
|
||||||
/// [time] is expected to be a [DateTime] with the hour at start of the hour.
|
|
||||||
@override
|
|
||||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
|
||||||
return time.add(Duration(hours: tickIncrement));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import 'base_time_stepper.dart';
|
|
||||||
|
|
||||||
/// Minute stepper where ticks generated aligns with the hour.
|
|
||||||
class MinuteTimeStepper extends BaseTimeStepper {
|
|
||||||
static const _defaultIncrements = [5, 10, 15, 20, 30];
|
|
||||||
static const _millisecondsInMinute = 60 * 1000;
|
|
||||||
|
|
||||||
final List<int> _allowedTickIncrements;
|
|
||||||
|
|
||||||
MinuteTimeStepper._internal(
|
|
||||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
|
||||||
: _allowedTickIncrements = increments,
|
|
||||||
super(dateTimeFactory);
|
|
||||||
|
|
||||||
factory MinuteTimeStepper(DateTimeFactory dateTimeFactory,
|
|
||||||
{List<int> allowedTickIncrements}) {
|
|
||||||
// Set the default increments if null.
|
|
||||||
allowedTickIncrements ??= _defaultIncrements;
|
|
||||||
|
|
||||||
// Must have at least one increment
|
|
||||||
assert(allowedTickIncrements.isNotEmpty);
|
|
||||||
// Increment must be between 1 and 60 inclusive.
|
|
||||||
assert(allowedTickIncrements
|
|
||||||
.any((increment) => increment <= 0 || increment > 60) ==
|
|
||||||
false);
|
|
||||||
|
|
||||||
return MinuteTimeStepper._internal(dateTimeFactory, allowedTickIncrements);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get typicalStepSizeMs => _millisecondsInMinute;
|
|
||||||
|
|
||||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
|
||||||
|
|
||||||
/// Picks a tick start time that guarantees the start of the hour is included.
|
|
||||||
///
|
|
||||||
/// Ex. Time is 3:46, increments is 5 minutes, step before is 3:45, because
|
|
||||||
/// we can guarantee a step at 4:00.
|
|
||||||
@override
|
|
||||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
|
||||||
final nextHourStart = time.millisecondsSinceEpoch +
|
|
||||||
(60 - time.minute) * _millisecondsInMinute;
|
|
||||||
|
|
||||||
final minutesToNextHour =
|
|
||||||
((nextHourStart - time.millisecondsSinceEpoch) / _millisecondsInMinute)
|
|
||||||
.ceil();
|
|
||||||
|
|
||||||
final minRemainder = minutesToNextHour % tickIncrement;
|
|
||||||
final rewindMinutes = minRemainder == 0 ? 0 : tickIncrement - minRemainder;
|
|
||||||
|
|
||||||
final stepBefore = dateTimeFactory.createDateTimeFromMilliSecondsSinceEpoch(
|
|
||||||
time.millisecondsSinceEpoch - rewindMinutes * _millisecondsInMinute);
|
|
||||||
|
|
||||||
return stepBefore;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
|
||||||
return time.add(Duration(minutes: tickIncrement));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import 'base_time_stepper.dart' show BaseTimeStepper;
|
|
||||||
|
|
||||||
/// Month stepper.
|
|
||||||
class MonthTimeStepper extends BaseTimeStepper {
|
|
||||||
static const _defaultIncrements = [1, 2, 3, 4, 6, 12];
|
|
||||||
|
|
||||||
final List<int> _allowedTickIncrements;
|
|
||||||
|
|
||||||
MonthTimeStepper._internal(
|
|
||||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
|
||||||
: _allowedTickIncrements = increments,
|
|
||||||
super(dateTimeFactory);
|
|
||||||
|
|
||||||
factory MonthTimeStepper(DateTimeFactory dateTimeFactory,
|
|
||||||
{List<int> allowedTickIncrements}) {
|
|
||||||
// Set the default increments if null.
|
|
||||||
allowedTickIncrements ??= _defaultIncrements;
|
|
||||||
|
|
||||||
// Must have at least one increment option.
|
|
||||||
assert(allowedTickIncrements.isNotEmpty);
|
|
||||||
// All increments must be > 0.
|
|
||||||
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
|
|
||||||
|
|
||||||
return MonthTimeStepper._internal(dateTimeFactory, allowedTickIncrements);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get typicalStepSizeMs => 30 * 24 * 3600 * 1000;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
|
||||||
|
|
||||||
/// Guarantee a step ending in the last month of the year.
|
|
||||||
///
|
|
||||||
/// If date is 2017 Oct and increments is 6, the step before is 2017 June.
|
|
||||||
@override
|
|
||||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
|
||||||
final monthRemainder = time.month % tickIncrement;
|
|
||||||
var newMonth = (time.month - monthRemainder) % DateTime.monthsPerYear;
|
|
||||||
// Handles the last month of the year (December) edge case.
|
|
||||||
// Ex. When month is December and increment is 1
|
|
||||||
if (time.month == DateTime.monthsPerYear && newMonth == 0) {
|
|
||||||
newMonth = DateTime.monthsPerYear;
|
|
||||||
}
|
|
||||||
final newYear =
|
|
||||||
time.year - (monthRemainder / DateTime.monthsPerYear).floor();
|
|
||||||
|
|
||||||
return dateTimeFactory.createDateTime(newYear, newMonth);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
|
||||||
final incrementedMonth = time.month + tickIncrement;
|
|
||||||
final newMonth = incrementedMonth % DateTime.monthsPerYear;
|
|
||||||
final newYear =
|
|
||||||
time.year + (incrementedMonth / DateTime.monthsPerYear).floor();
|
|
||||||
|
|
||||||
return dateTimeFactory.createDateTime(newYear, newMonth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../tick_provider.dart' show BaseTickProvider;
|
|
||||||
import '../time/date_time_extents.dart' show DateTimeExtents;
|
|
||||||
|
|
||||||
/// Provides ticks for a particular time unit.
|
|
||||||
///
|
|
||||||
/// Used by [AutoAdjustingDateTimeTickProvider].
|
|
||||||
abstract class TimeRangeTickProvider extends BaseTickProvider<DateTime> {
|
|
||||||
/// Returns if this tick provider will produce a sufficient number of ticks
|
|
||||||
/// for [domainExtents].
|
|
||||||
bool providesSufficientTicksForRange(DateTimeExtents domainExtents);
|
|
||||||
|
|
||||||
/// Find the closet step size, from provided step size, in milliseconds.
|
|
||||||
int getClosestStepSize(int stepSize);
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../axis.dart' show AxisOrientation;
|
|
||||||
import '../draw_strategy/tick_draw_strategy.dart' show TickDrawStrategy;
|
|
||||||
import '../tick.dart' show Tick;
|
|
||||||
import '../tick_formatter.dart' show TickFormatter;
|
|
||||||
import '../tick_provider.dart' show TickHint;
|
|
||||||
import 'date_time_extents.dart' show DateTimeExtents;
|
|
||||||
import 'date_time_scale.dart' show DateTimeScale;
|
|
||||||
import 'time_range_tick_provider.dart' show TimeRangeTickProvider;
|
|
||||||
import 'time_stepper.dart' show TimeStepper;
|
|
||||||
|
|
||||||
// Contains all the common code for the time range tick providers.
|
|
||||||
class TimeRangeTickProviderImpl extends TimeRangeTickProvider {
|
|
||||||
final int requiredMinimumTicks;
|
|
||||||
final TimeStepper timeStepper;
|
|
||||||
|
|
||||||
TimeRangeTickProviderImpl(this.timeStepper, {this.requiredMinimumTicks = 3});
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool providesSufficientTicksForRange(DateTimeExtents domainExtents) {
|
|
||||||
final cnt = timeStepper.getStepCountBetween(domainExtents, 1);
|
|
||||||
return cnt >= requiredMinimumTicks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the closet step size, from provided step size, in milliseconds.
|
|
||||||
@override
|
|
||||||
int getClosestStepSize(int stepSize) {
|
|
||||||
return timeStepper.typicalStepSizeMs *
|
|
||||||
_getClosestIncrementFromStepSize(stepSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the increment that is closest to the step size.
|
|
||||||
int _getClosestIncrementFromStepSize(int stepSize) {
|
|
||||||
int minDifference;
|
|
||||||
int closestIncrement;
|
|
||||||
|
|
||||||
for (int increment in timeStepper.allowedTickIncrements) {
|
|
||||||
final difference =
|
|
||||||
(stepSize - (timeStepper.typicalStepSizeMs * increment)).abs();
|
|
||||||
if (minDifference == null || minDifference > difference) {
|
|
||||||
minDifference = difference;
|
|
||||||
closestIncrement = increment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return closestIncrement;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Tick<DateTime>> getTicks({
|
|
||||||
@required ChartContext context,
|
|
||||||
@required GraphicsFactory graphicsFactory,
|
|
||||||
@required DateTimeScale scale,
|
|
||||||
@required TickFormatter<DateTime> formatter,
|
|
||||||
@required Map<DateTime, String> formatterValueCache,
|
|
||||||
@required TickDrawStrategy tickDrawStrategy,
|
|
||||||
@required AxisOrientation orientation,
|
|
||||||
bool viewportExtensionEnabled = false,
|
|
||||||
TickHint<DateTime> tickHint,
|
|
||||||
}) {
|
|
||||||
List<Tick<DateTime>> currentTicks;
|
|
||||||
final tickValues = <DateTime>[];
|
|
||||||
final timeStepIt = timeStepper.getSteps(scale.viewportDomain).iterator;
|
|
||||||
|
|
||||||
// Try different tickIncrements and choose the first that has no collisions.
|
|
||||||
// If none exist use the last one which should have the fewest ticks and
|
|
||||||
// hope that the renderer will resolve collisions.
|
|
||||||
//
|
|
||||||
// If a tick hint was provided, use the tick hint to search for the closest
|
|
||||||
// increment and use that.
|
|
||||||
List<int> allowedTickIncrements;
|
|
||||||
if (tickHint != null) {
|
|
||||||
final stepSize = tickHint.end.difference(tickHint.start).inMilliseconds;
|
|
||||||
allowedTickIncrements = [_getClosestIncrementFromStepSize(stepSize)];
|
|
||||||
} else {
|
|
||||||
allowedTickIncrements = timeStepper.allowedTickIncrements;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < allowedTickIncrements.length; i++) {
|
|
||||||
// Create tick values with a specified increment.
|
|
||||||
final tickIncrement = allowedTickIncrements[i];
|
|
||||||
tickValues.clear();
|
|
||||||
timeStepIt.reset(tickIncrement);
|
|
||||||
while (timeStepIt.moveNext()) {
|
|
||||||
tickValues.add(timeStepIt.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ticks
|
|
||||||
currentTicks = createTicks(tickValues,
|
|
||||||
context: context,
|
|
||||||
graphicsFactory: graphicsFactory,
|
|
||||||
scale: scale,
|
|
||||||
formatter: formatter,
|
|
||||||
formatterValueCache: formatterValueCache,
|
|
||||||
tickDrawStrategy: tickDrawStrategy,
|
|
||||||
stepSize: timeStepper.typicalStepSizeMs * tickIncrement);
|
|
||||||
|
|
||||||
// Request collision check from draw strategy.
|
|
||||||
final collisionReport =
|
|
||||||
tickDrawStrategy.collides(currentTicks, orientation);
|
|
||||||
|
|
||||||
if (!collisionReport.ticksCollide) {
|
|
||||||
// Return the first non colliding ticks.
|
|
||||||
return currentTicks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all ticks collide, return the last generated ticks.
|
|
||||||
return currentTicks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'date_time_extents.dart' show DateTimeExtents;
|
|
||||||
|
|
||||||
/// Represents the step/tick information for the given time range.
|
|
||||||
abstract class TimeStepper {
|
|
||||||
/// Get new bounding extents to the ticks that would contain the given
|
|
||||||
/// timeExtents.
|
|
||||||
DateTimeExtents updateBoundingSteps(DateTimeExtents timeExtents);
|
|
||||||
|
|
||||||
/// Returns the number steps/ticks are between the given extents inclusive.
|
|
||||||
///
|
|
||||||
/// Does not extend the extents to the bounding ticks.
|
|
||||||
int getStepCountBetween(DateTimeExtents timeExtents, int tickIncrement);
|
|
||||||
|
|
||||||
/// Generates an Iterable for iterating over the time steps bounded by the
|
|
||||||
/// given timeExtents. The desired tickIncrement can be set on the returned
|
|
||||||
/// [TimeStepIteratorFactory].
|
|
||||||
TimeStepIteratorFactory getSteps(DateTimeExtents timeExtents);
|
|
||||||
|
|
||||||
/// Returns the typical stepSize for this stepper assuming increment by 1.
|
|
||||||
int get typicalStepSizeMs;
|
|
||||||
|
|
||||||
/// An ordered list of step increments that makes sense given the step.
|
|
||||||
///
|
|
||||||
/// Example: hours may increment by 1, 2, 3, 4, 6, 12. It doesn't make sense
|
|
||||||
/// to increment hours by 7.
|
|
||||||
List<int> get allowedTickIncrements;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterator with a reset function that can be used multiple times to avoid
|
|
||||||
/// object instantiation during the Android layout/draw phases.
|
|
||||||
abstract class TimeStepIterator extends Iterator<DateTime> {
|
|
||||||
/// Reset the iterator and set the tickIncrement to the specified value.
|
|
||||||
///
|
|
||||||
/// This method is provided so that the same iterator instance can be used for
|
|
||||||
/// different tick increments, avoiding object allocation during Android
|
|
||||||
/// layout/draw phases.
|
|
||||||
TimeStepIterator reset(int tickIncrement);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Factory that creates TimeStepIterator with the set tickIncrement value.
|
|
||||||
abstract class TimeStepIteratorFactory extends Iterable {
|
|
||||||
/// Get iterator and optionally set the tickIncrement.
|
|
||||||
@override
|
|
||||||
TimeStepIterator get iterator;
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
/// Formatter of [DateTime] ticks
|
|
||||||
abstract class TimeTickFormatter {
|
|
||||||
/// Format for tick that is the first in a set of ticks.
|
|
||||||
String formatFirstTick(DateTime date);
|
|
||||||
|
|
||||||
/// Format for a 'simple' tick.
|
|
||||||
///
|
|
||||||
/// Ex. Not a first tick or transition tick.
|
|
||||||
String formatSimpleTick(DateTime date);
|
|
||||||
|
|
||||||
/// Format for a transitional tick.
|
|
||||||
String formatTransitionTick(DateTime date);
|
|
||||||
|
|
||||||
/// Returns true if tick is a transitional tick.
|
|
||||||
bool isTransition(DateTime tickValue, DateTime prevTickValue);
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:intl/intl.dart' show DateFormat;
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import 'time_tick_formatter.dart' show TimeTickFormatter;
|
|
||||||
|
|
||||||
/// Formatter that can format simple and transition time ticks differently.
|
|
||||||
class TimeTickFormatterImpl implements TimeTickFormatter {
|
|
||||||
DateFormat _simpleFormat;
|
|
||||||
DateFormat _transitionFormat;
|
|
||||||
final CalendarField transitionField;
|
|
||||||
|
|
||||||
/// Create time tick formatter.
|
|
||||||
///
|
|
||||||
/// [dateTimeFactory] factory to use to generate the [DateFormat].
|
|
||||||
/// [simpleFormat] format to use for most ticks.
|
|
||||||
/// [transitionFormat] format to use when the time unit transitions.
|
|
||||||
/// For example showing the month with the date for Jan 1.
|
|
||||||
/// [transitionField] the calendar field that indicates transition.
|
|
||||||
TimeTickFormatterImpl(
|
|
||||||
{@required DateTimeFactory dateTimeFactory,
|
|
||||||
@required String simpleFormat,
|
|
||||||
@required String transitionFormat,
|
|
||||||
this.transitionField}) {
|
|
||||||
_simpleFormat = dateTimeFactory.createDateFormat(simpleFormat);
|
|
||||||
_transitionFormat = dateTimeFactory.createDateFormat(transitionFormat);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String formatFirstTick(DateTime date) => _transitionFormat.format(date);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String formatSimpleTick(DateTime date) => _simpleFormat.format(date);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String formatTransitionTick(DateTime date) => _transitionFormat.format(date);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool isTransition(DateTime tickValue, DateTime prevTickValue) {
|
|
||||||
// Transition is always false if no transition field is specified.
|
|
||||||
if (transitionField == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final prevTransitionFieldValue =
|
|
||||||
getCalendarField(prevTickValue, transitionField);
|
|
||||||
final transitionFieldValue = getCalendarField(tickValue, transitionField);
|
|
||||||
return prevTransitionFieldValue != transitionFieldValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the calendar field for [dateTime].
|
|
||||||
int getCalendarField(DateTime dateTime, CalendarField field) {
|
|
||||||
int value;
|
|
||||||
|
|
||||||
switch (field) {
|
|
||||||
case CalendarField.year:
|
|
||||||
value = dateTime.year;
|
|
||||||
break;
|
|
||||||
case CalendarField.month:
|
|
||||||
value = dateTime.month;
|
|
||||||
break;
|
|
||||||
case CalendarField.date:
|
|
||||||
value = dateTime.day;
|
|
||||||
break;
|
|
||||||
case CalendarField.hourOfDay:
|
|
||||||
value = dateTime.hour;
|
|
||||||
break;
|
|
||||||
case CalendarField.minute:
|
|
||||||
value = dateTime.minute;
|
|
||||||
break;
|
|
||||||
case CalendarField.second:
|
|
||||||
value = dateTime.second;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CalendarField {
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
date,
|
|
||||||
hourOfDay,
|
|
||||||
minute,
|
|
||||||
second,
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/date_time_factory.dart' show DateTimeFactory;
|
|
||||||
import 'base_time_stepper.dart' show BaseTimeStepper;
|
|
||||||
|
|
||||||
/// Year stepper.
|
|
||||||
class YearTimeStepper extends BaseTimeStepper {
|
|
||||||
static const _defaultIncrements = [1, 2, 5, 10, 50, 100, 500, 1000];
|
|
||||||
|
|
||||||
final List<int> _allowedTickIncrements;
|
|
||||||
|
|
||||||
YearTimeStepper._internal(
|
|
||||||
DateTimeFactory dateTimeFactory, List<int> increments)
|
|
||||||
: _allowedTickIncrements = increments,
|
|
||||||
super(dateTimeFactory);
|
|
||||||
|
|
||||||
factory YearTimeStepper(DateTimeFactory dateTimeFactory,
|
|
||||||
{List<int> allowedTickIncrements}) {
|
|
||||||
// Set the default increments if null.
|
|
||||||
allowedTickIncrements ??= _defaultIncrements;
|
|
||||||
|
|
||||||
// Must have at least one increment option.
|
|
||||||
assert(allowedTickIncrements.isNotEmpty);
|
|
||||||
// All increments must be > 0.
|
|
||||||
assert(allowedTickIncrements.any((increment) => increment <= 0) == false);
|
|
||||||
|
|
||||||
return YearTimeStepper._internal(dateTimeFactory, allowedTickIncrements);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get typicalStepSizeMs => 365 * 24 * 3600 * 1000;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<int> get allowedTickIncrements => _allowedTickIncrements;
|
|
||||||
|
|
||||||
/// Guarantees the increment is a factor of the tick value.
|
|
||||||
///
|
|
||||||
/// Example: 2017, tick increment of 10, step before is 2010.
|
|
||||||
@override
|
|
||||||
DateTime getStepTimeBeforeInclusive(DateTime time, int tickIncrement) {
|
|
||||||
final yearRemainder = time.year % tickIncrement;
|
|
||||||
return dateTimeFactory.createDateTime(time.year - yearRemainder);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
DateTime getNextStepTime(DateTime time, int tickIncrement) {
|
|
||||||
return dateTimeFactory.createDateTime(time.year + tickIncrement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,466 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:collection' show LinkedHashMap;
|
|
||||||
import 'dart:math' show Point;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show protected;
|
|
||||||
|
|
||||||
import '../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../data/series.dart' show Series;
|
|
||||||
import '../bar/bar_renderer.dart' show BarRenderer;
|
|
||||||
import '../common/base_chart.dart' show BaseChart;
|
|
||||||
import '../common/chart_context.dart' show ChartContext;
|
|
||||||
import '../common/datum_details.dart' show DatumDetails;
|
|
||||||
import '../common/processed_series.dart' show MutableSeries;
|
|
||||||
import '../common/selection_model/selection_model.dart' show SelectionModelType;
|
|
||||||
import '../common/series_renderer.dart' show SeriesRenderer;
|
|
||||||
import '../layout/layout_config.dart' show LayoutConfig, MarginSpec;
|
|
||||||
import '../layout/layout_view.dart' show LayoutViewPaintOrder;
|
|
||||||
import 'axis/axis.dart'
|
|
||||||
show
|
|
||||||
Axis,
|
|
||||||
AxisOrientation,
|
|
||||||
OrdinalAxis,
|
|
||||||
NumericAxis,
|
|
||||||
domainAxisKey,
|
|
||||||
measureAxisIdKey,
|
|
||||||
measureAxisKey;
|
|
||||||
import 'axis/draw_strategy/gridline_draw_strategy.dart'
|
|
||||||
show GridlineRendererSpec;
|
|
||||||
import 'axis/draw_strategy/none_draw_strategy.dart' show NoneDrawStrategy;
|
|
||||||
import 'axis/draw_strategy/small_tick_draw_strategy.dart'
|
|
||||||
show SmallTickRendererSpec;
|
|
||||||
import 'axis/spec/axis_spec.dart' show AxisSpec;
|
|
||||||
|
|
||||||
class NumericCartesianChart extends CartesianChart<num> {
|
|
||||||
NumericCartesianChart(
|
|
||||||
{bool vertical,
|
|
||||||
LayoutConfig layoutConfig,
|
|
||||||
NumericAxis primaryMeasureAxis,
|
|
||||||
NumericAxis secondaryMeasureAxis,
|
|
||||||
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
|
|
||||||
: super(
|
|
||||||
vertical: vertical,
|
|
||||||
layoutConfig: layoutConfig,
|
|
||||||
domainAxis: NumericAxis(),
|
|
||||||
primaryMeasureAxis: primaryMeasureAxis,
|
|
||||||
secondaryMeasureAxis: secondaryMeasureAxis,
|
|
||||||
disjointMeasureAxes: disjointMeasureAxes);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void initDomainAxis() {
|
|
||||||
_domainAxis.tickDrawStrategy = SmallTickRendererSpec<num>()
|
|
||||||
.createDrawStrategy(context, graphicsFactory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class OrdinalCartesianChart extends CartesianChart<String> {
|
|
||||||
OrdinalCartesianChart(
|
|
||||||
{bool vertical,
|
|
||||||
LayoutConfig layoutConfig,
|
|
||||||
NumericAxis primaryMeasureAxis,
|
|
||||||
NumericAxis secondaryMeasureAxis,
|
|
||||||
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
|
|
||||||
: super(
|
|
||||||
vertical: vertical,
|
|
||||||
layoutConfig: layoutConfig,
|
|
||||||
domainAxis: OrdinalAxis(),
|
|
||||||
primaryMeasureAxis: primaryMeasureAxis,
|
|
||||||
secondaryMeasureAxis: secondaryMeasureAxis,
|
|
||||||
disjointMeasureAxes: disjointMeasureAxes);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
void initDomainAxis() {
|
|
||||||
_domainAxis
|
|
||||||
..tickDrawStrategy = SmallTickRendererSpec<String>()
|
|
||||||
.createDrawStrategy(context, graphicsFactory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class CartesianChart<D> extends BaseChart<D> {
|
|
||||||
static final _defaultLayoutConfig = LayoutConfig(
|
|
||||||
topSpec: MarginSpec.fromPixel(minPixel: 20),
|
|
||||||
bottomSpec: MarginSpec.fromPixel(minPixel: 20),
|
|
||||||
leftSpec: MarginSpec.fromPixel(minPixel: 20),
|
|
||||||
rightSpec: MarginSpec.fromPixel(minPixel: 20),
|
|
||||||
);
|
|
||||||
|
|
||||||
bool vertical;
|
|
||||||
|
|
||||||
/// The current domain axis for this chart.
|
|
||||||
Axis<D> _domainAxis;
|
|
||||||
|
|
||||||
/// Temporarily stores the new domain axis that is passed in the constructor
|
|
||||||
/// and the new domain axis created when [domainAxisSpec] is set to a new
|
|
||||||
/// spec.
|
|
||||||
///
|
|
||||||
/// This step is necessary because the axis cannot be fully configured until
|
|
||||||
/// [context] is available. [configurationChanged] is called after [context]
|
|
||||||
/// is available and [_newDomainAxis] will be set to [_domainAxis] and then
|
|
||||||
/// reset back to null.
|
|
||||||
Axis<D> _newDomainAxis;
|
|
||||||
|
|
||||||
/// The current domain axis spec that was used to configure [_domainAxis].
|
|
||||||
///
|
|
||||||
/// This is kept to check if the axis spec has changed when [domainAxisSpec]
|
|
||||||
/// is set.
|
|
||||||
AxisSpec<D> _domainAxisSpec;
|
|
||||||
|
|
||||||
/// Temporarily stores the new domain axis spec that is passed in when
|
|
||||||
/// [domainAxisSpec] is set and is different from [_domainAxisSpec]. This spec
|
|
||||||
/// is then applied to the new domain axis when [configurationChanged] is
|
|
||||||
/// called.
|
|
||||||
AxisSpec<D> _newDomainAxisSpec;
|
|
||||||
|
|
||||||
final Axis<num> _primaryMeasureAxis;
|
|
||||||
|
|
||||||
final Axis<num> _secondaryMeasureAxis;
|
|
||||||
|
|
||||||
final LinkedHashMap<String, NumericAxis> _disjointMeasureAxes;
|
|
||||||
|
|
||||||
/// If set to true, the vertical axis will render the opposite of the default
|
|
||||||
/// direction.
|
|
||||||
bool flipVerticalAxisOutput = false;
|
|
||||||
|
|
||||||
bool _usePrimaryMeasureAxis = false;
|
|
||||||
bool _useSecondaryMeasureAxis = false;
|
|
||||||
|
|
||||||
CartesianChart(
|
|
||||||
{bool vertical,
|
|
||||||
LayoutConfig layoutConfig,
|
|
||||||
Axis<D> domainAxis,
|
|
||||||
NumericAxis primaryMeasureAxis,
|
|
||||||
NumericAxis secondaryMeasureAxis,
|
|
||||||
LinkedHashMap<String, NumericAxis> disjointMeasureAxes})
|
|
||||||
: vertical = vertical ?? true,
|
|
||||||
// [domainAxis] will be set to the new axis in [configurationChanged].
|
|
||||||
_newDomainAxis = domainAxis,
|
|
||||||
_primaryMeasureAxis = primaryMeasureAxis ?? NumericAxis(),
|
|
||||||
_secondaryMeasureAxis = secondaryMeasureAxis ?? NumericAxis(),
|
|
||||||
_disjointMeasureAxes = disjointMeasureAxes ?? <String, NumericAxis>{},
|
|
||||||
super(layoutConfig: layoutConfig ?? _defaultLayoutConfig) {
|
|
||||||
// As a convenience for chart configuration, set the paint order on any axis
|
|
||||||
// that is missing one.
|
|
||||||
_primaryMeasureAxis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
|
|
||||||
_secondaryMeasureAxis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
|
|
||||||
|
|
||||||
_disjointMeasureAxes.forEach((axisId, axis) {
|
|
||||||
axis.layoutPaintOrder ??= LayoutViewPaintOrder.measureAxis;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void init(ChartContext context, GraphicsFactory graphicsFactory) {
|
|
||||||
super.init(context, graphicsFactory);
|
|
||||||
|
|
||||||
_primaryMeasureAxis.context = context;
|
|
||||||
_primaryMeasureAxis.tickDrawStrategy = GridlineRendererSpec<num>()
|
|
||||||
.createDrawStrategy(context, graphicsFactory);
|
|
||||||
|
|
||||||
_secondaryMeasureAxis.context = context;
|
|
||||||
_secondaryMeasureAxis.tickDrawStrategy = GridlineRendererSpec<num>()
|
|
||||||
.createDrawStrategy(context, graphicsFactory);
|
|
||||||
|
|
||||||
_disjointMeasureAxes.forEach((axisId, axis) {
|
|
||||||
axis.context = context;
|
|
||||||
axis.tickDrawStrategy = NoneDrawStrategy<num>(context, graphicsFactory);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Axis get domainAxis => _domainAxis;
|
|
||||||
|
|
||||||
/// Allows the chart to configure the domain axis when it is created.
|
|
||||||
@protected
|
|
||||||
void initDomainAxis();
|
|
||||||
|
|
||||||
/// Create a new domain axis and save the new spec to be applied during
|
|
||||||
/// [configurationChanged].
|
|
||||||
set domainAxisSpec(AxisSpec axisSpec) {
|
|
||||||
if (_domainAxisSpec != axisSpec) {
|
|
||||||
_newDomainAxis = createDomainAxisFromSpec(axisSpec);
|
|
||||||
_newDomainAxisSpec = axisSpec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates the domain axis spec from provided axis spec.
|
|
||||||
@protected
|
|
||||||
Axis<D> createDomainAxisFromSpec(AxisSpec<D> axisSpec) {
|
|
||||||
return axisSpec.createAxis();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void configurationChanged() {
|
|
||||||
if (_newDomainAxis != null) {
|
|
||||||
if (_domainAxis != null) {
|
|
||||||
removeView(_domainAxis);
|
|
||||||
}
|
|
||||||
|
|
||||||
_domainAxis = _newDomainAxis;
|
|
||||||
_domainAxis
|
|
||||||
..context = context
|
|
||||||
..layoutPaintOrder = LayoutViewPaintOrder.domainAxis;
|
|
||||||
|
|
||||||
initDomainAxis();
|
|
||||||
|
|
||||||
addView(_domainAxis);
|
|
||||||
|
|
||||||
_newDomainAxis = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_newDomainAxisSpec != null) {
|
|
||||||
_domainAxisSpec = _newDomainAxisSpec;
|
|
||||||
_newDomainAxisSpec.configure(_domainAxis, context, graphicsFactory);
|
|
||||||
_newDomainAxisSpec = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the measure axis matching the provided id.
|
|
||||||
///
|
|
||||||
/// If none is provided, this returns the primary measure axis.
|
|
||||||
Axis getMeasureAxis({String axisId}) {
|
|
||||||
Axis axis;
|
|
||||||
if (axisId == Axis.secondaryMeasureAxisId) {
|
|
||||||
axis = _secondaryMeasureAxis;
|
|
||||||
} else if (axisId == Axis.primaryMeasureAxisId) {
|
|
||||||
axis = _primaryMeasureAxis;
|
|
||||||
} else if (_disjointMeasureAxes[axisId] != null) {
|
|
||||||
axis = _disjointMeasureAxes[axisId];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no valid axisId was provided, fall back to primary axis.
|
|
||||||
axis ??= _primaryMeasureAxis;
|
|
||||||
|
|
||||||
return axis;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Change measure axis spec to create new measure axis.
|
|
||||||
/// Sets the primary measure axis for the chart, rendered on the start side of
|
|
||||||
/// the domain axis.
|
|
||||||
set primaryMeasureAxisSpec(AxisSpec axisSpec) {
|
|
||||||
axisSpec.configure(_primaryMeasureAxis, context, graphicsFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the secondary measure axis for the chart, rendered on the end side of
|
|
||||||
/// the domain axis.
|
|
||||||
set secondaryMeasureAxisSpec(AxisSpec axisSpec) {
|
|
||||||
axisSpec.configure(_secondaryMeasureAxis, context, graphicsFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a map of disjoint measure axes for the chart.
|
|
||||||
///
|
|
||||||
/// Disjoint measure axes can be used to scale a sub-set of series on the
|
|
||||||
/// chart independently from the primary and secondary axes. The general use
|
|
||||||
/// case for this type of chart is to show differences in the trends of the
|
|
||||||
/// data, without comparing their absolute values.
|
|
||||||
///
|
|
||||||
/// Disjoint axes will not render any tick or gridline elements. With
|
|
||||||
/// independent scales, there would be a lot of collision in labels were they
|
|
||||||
/// to do so.
|
|
||||||
///
|
|
||||||
/// If any series is rendered with a disjoint axis, it is highly recommended
|
|
||||||
/// to render all series with disjoint axes. Otherwise, the chart may be
|
|
||||||
/// visually misleading.
|
|
||||||
///
|
|
||||||
/// A [LinkedHashMap] is used to ensure consistent ordering when painting the
|
|
||||||
/// axes.
|
|
||||||
set disjointMeasureAxisSpecs(LinkedHashMap<String, AxisSpec> axisSpecs) {
|
|
||||||
axisSpecs.forEach((axisId, axisSpec) {
|
|
||||||
axisSpec.configure(
|
|
||||||
_disjointMeasureAxes[axisId], context, graphicsFactory);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
MutableSeries<D> makeSeries(Series<dynamic, D> series) {
|
|
||||||
MutableSeries<D> s = super.makeSeries(series);
|
|
||||||
|
|
||||||
s.measureOffsetFn ??= (_) => 0;
|
|
||||||
|
|
||||||
// Setup the Axes
|
|
||||||
s.setAttr(domainAxisKey, domainAxis);
|
|
||||||
s.setAttr(measureAxisKey,
|
|
||||||
getMeasureAxis(axisId: series.getAttribute(measureAxisIdKey)));
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
SeriesRenderer<D> makeDefaultRenderer() {
|
|
||||||
return BarRenderer()..rendererId = SeriesRenderer.defaultRendererId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, List<MutableSeries<D>>> preprocessSeries(
|
|
||||||
List<MutableSeries<D>> seriesList) {
|
|
||||||
var rendererToSeriesList = super.preprocessSeries(seriesList);
|
|
||||||
|
|
||||||
// Check if primary or secondary measure axis is being used.
|
|
||||||
for (final series in seriesList) {
|
|
||||||
final measureAxisId = series.getAttr(measureAxisIdKey);
|
|
||||||
_usePrimaryMeasureAxis = _usePrimaryMeasureAxis ||
|
|
||||||
(measureAxisId == null || measureAxisId == Axis.primaryMeasureAxisId);
|
|
||||||
_useSecondaryMeasureAxis = _useSecondaryMeasureAxis ||
|
|
||||||
(measureAxisId == Axis.secondaryMeasureAxisId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add or remove the primary axis view.
|
|
||||||
if (_usePrimaryMeasureAxis) {
|
|
||||||
addView(_primaryMeasureAxis);
|
|
||||||
} else {
|
|
||||||
removeView(_primaryMeasureAxis);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add or remove the secondary axis view.
|
|
||||||
if (_useSecondaryMeasureAxis) {
|
|
||||||
addView(_secondaryMeasureAxis);
|
|
||||||
} else {
|
|
||||||
removeView(_secondaryMeasureAxis);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add all disjoint axis views so that their range will be configured.
|
|
||||||
_disjointMeasureAxes.forEach((axisId, axis) {
|
|
||||||
addView(axis);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset stale values from previous draw cycles.
|
|
||||||
domainAxis.resetDomains();
|
|
||||||
_primaryMeasureAxis.resetDomains();
|
|
||||||
_secondaryMeasureAxis.resetDomains();
|
|
||||||
|
|
||||||
_disjointMeasureAxes.forEach((axisId, axis) {
|
|
||||||
axis.resetDomains();
|
|
||||||
});
|
|
||||||
|
|
||||||
final reverseAxisDirection = context != null && context.isRtl;
|
|
||||||
|
|
||||||
if (vertical) {
|
|
||||||
domainAxis
|
|
||||||
..axisOrientation = AxisOrientation.bottom
|
|
||||||
..reverseOutputRange = reverseAxisDirection;
|
|
||||||
|
|
||||||
_primaryMeasureAxis
|
|
||||||
..axisOrientation = (reverseAxisDirection
|
|
||||||
? AxisOrientation.right
|
|
||||||
: AxisOrientation.left)
|
|
||||||
..reverseOutputRange = flipVerticalAxisOutput;
|
|
||||||
|
|
||||||
_secondaryMeasureAxis
|
|
||||||
..axisOrientation = (reverseAxisDirection
|
|
||||||
? AxisOrientation.left
|
|
||||||
: AxisOrientation.right)
|
|
||||||
..reverseOutputRange = flipVerticalAxisOutput;
|
|
||||||
|
|
||||||
_disjointMeasureAxes.forEach((axisId, axis) {
|
|
||||||
axis
|
|
||||||
..axisOrientation = (reverseAxisDirection
|
|
||||||
? AxisOrientation.left
|
|
||||||
: AxisOrientation.right)
|
|
||||||
..reverseOutputRange = flipVerticalAxisOutput;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
domainAxis
|
|
||||||
..axisOrientation = (reverseAxisDirection
|
|
||||||
? AxisOrientation.right
|
|
||||||
: AxisOrientation.left)
|
|
||||||
..reverseOutputRange = flipVerticalAxisOutput;
|
|
||||||
|
|
||||||
_primaryMeasureAxis
|
|
||||||
..axisOrientation = AxisOrientation.bottom
|
|
||||||
..reverseOutputRange = reverseAxisDirection;
|
|
||||||
|
|
||||||
_secondaryMeasureAxis
|
|
||||||
..axisOrientation = AxisOrientation.top
|
|
||||||
..reverseOutputRange = reverseAxisDirection;
|
|
||||||
|
|
||||||
_disjointMeasureAxes.forEach((axisId, axis) {
|
|
||||||
axis
|
|
||||||
..axisOrientation = AxisOrientation.top
|
|
||||||
..reverseOutputRange = reverseAxisDirection;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Have each renderer configure the axes with their domain and measure
|
|
||||||
// values.
|
|
||||||
rendererToSeriesList.forEach((rendererId, seriesList) {
|
|
||||||
getSeriesRenderer(rendererId).configureDomainAxes(seriesList);
|
|
||||||
getSeriesRenderer(rendererId).configureMeasureAxes(seriesList);
|
|
||||||
});
|
|
||||||
|
|
||||||
return rendererToSeriesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onSkipLayout() {
|
|
||||||
// Update ticks only when skipping layout.
|
|
||||||
domainAxis.updateTicks();
|
|
||||||
|
|
||||||
if (_usePrimaryMeasureAxis) {
|
|
||||||
_primaryMeasureAxis.updateTicks();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_useSecondaryMeasureAxis) {
|
|
||||||
_secondaryMeasureAxis.updateTicks();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disjointMeasureAxes.forEach((axisId, axis) {
|
|
||||||
axis.updateTicks();
|
|
||||||
});
|
|
||||||
|
|
||||||
super.onSkipLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onPostLayout(Map<String, List<MutableSeries<D>>> rendererToSeriesList) {
|
|
||||||
fireOnAxisConfigured();
|
|
||||||
|
|
||||||
super.onPostLayout(rendererToSeriesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a list of datum details from selection model of [type].
|
|
||||||
@override
|
|
||||||
List<DatumDetails<D>> getDatumDetails(SelectionModelType type) {
|
|
||||||
final entries = <DatumDetails<D>>[];
|
|
||||||
|
|
||||||
getSelectionModel(type).selectedDatum.forEach((seriesDatum) {
|
|
||||||
final series = seriesDatum.series;
|
|
||||||
final datum = seriesDatum.datum;
|
|
||||||
final datumIndex = seriesDatum.index;
|
|
||||||
|
|
||||||
final domain = series.domainFn(datumIndex);
|
|
||||||
final measure = series.measureFn(datumIndex);
|
|
||||||
final rawMeasure = series.rawMeasureFn(datumIndex);
|
|
||||||
final color = series.colorFn(datumIndex);
|
|
||||||
|
|
||||||
final domainPosition = series.getAttr(domainAxisKey).getLocation(domain);
|
|
||||||
final measurePosition =
|
|
||||||
series.getAttr(measureAxisKey).getLocation(measure);
|
|
||||||
|
|
||||||
final chartPosition = Point<double>(
|
|
||||||
vertical ? domainPosition : measurePosition,
|
|
||||||
vertical ? measurePosition : domainPosition);
|
|
||||||
|
|
||||||
entries.add(DatumDetails(
|
|
||||||
datum: datum,
|
|
||||||
domain: domain,
|
|
||||||
measure: measure,
|
|
||||||
rawMeasure: rawMeasure,
|
|
||||||
series: series,
|
|
||||||
color: color,
|
|
||||||
chartPosition: chartPosition));
|
|
||||||
});
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
import '../../common/symbol_renderer.dart' show SymbolRenderer;
|
|
||||||
import '../../data/series.dart' show AccessorFn;
|
|
||||||
import '../common/base_chart.dart' show BaseChart;
|
|
||||||
import '../common/processed_series.dart' show MutableSeries;
|
|
||||||
import '../common/series_renderer.dart' show BaseSeriesRenderer, SeriesRenderer;
|
|
||||||
import 'axis/axis.dart' show Axis, domainAxisKey, measureAxisKey;
|
|
||||||
import 'cartesian_chart.dart' show CartesianChart;
|
|
||||||
|
|
||||||
abstract class CartesianRenderer<D> extends SeriesRenderer<D> {
|
|
||||||
void configureDomainAxes(List<MutableSeries<D>> seriesList);
|
|
||||||
|
|
||||||
void configureMeasureAxes(List<MutableSeries<D>> seriesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class BaseCartesianRenderer<D> extends BaseSeriesRenderer<D>
|
|
||||||
implements CartesianRenderer<D> {
|
|
||||||
bool _renderingVertically = true;
|
|
||||||
|
|
||||||
BaseCartesianRenderer(
|
|
||||||
{@required String rendererId,
|
|
||||||
@required int layoutPaintOrder,
|
|
||||||
SymbolRenderer symbolRenderer})
|
|
||||||
: super(
|
|
||||||
rendererId: rendererId,
|
|
||||||
layoutPaintOrder: layoutPaintOrder,
|
|
||||||
symbolRenderer: symbolRenderer);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onAttach(BaseChart<D> chart) {
|
|
||||||
super.onAttach(chart);
|
|
||||||
_renderingVertically = (chart as CartesianChart).vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get renderingVertically => _renderingVertically;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void configureDomainAxes(List<MutableSeries<D>> seriesList) {
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
if (series.data.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final domainAxis = series.getAttr(domainAxisKey);
|
|
||||||
final domainFn = series.domainFn;
|
|
||||||
final domainLowerBoundFn = series.domainLowerBoundFn;
|
|
||||||
final domainUpperBoundFn = series.domainUpperBoundFn;
|
|
||||||
|
|
||||||
if (domainAxis == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (renderingVertically) {
|
|
||||||
for (int i = 0; i < series.data.length; i++) {
|
|
||||||
domainAxis.addDomainValue(domainFn(i));
|
|
||||||
|
|
||||||
if (domainLowerBoundFn != null && domainUpperBoundFn != null) {
|
|
||||||
final domainLowerBound = domainLowerBoundFn(i);
|
|
||||||
final domainUpperBound = domainUpperBoundFn(i);
|
|
||||||
if (domainLowerBound != null && domainUpperBound != null) {
|
|
||||||
domainAxis.addDomainValue(domainLowerBound);
|
|
||||||
domainAxis.addDomainValue(domainUpperBound);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// When rendering horizontally, domains are displayed from top to bottom
|
|
||||||
// in order to match visual display in legend.
|
|
||||||
for (int i = series.data.length - 1; i >= 0; i--) {
|
|
||||||
domainAxis.addDomainValue(domainFn(i));
|
|
||||||
|
|
||||||
if (domainLowerBoundFn != null && domainUpperBoundFn != null) {
|
|
||||||
final domainLowerBound = domainLowerBoundFn(i);
|
|
||||||
final domainUpperBound = domainUpperBoundFn(i);
|
|
||||||
if (domainLowerBound != null && domainUpperBound != null) {
|
|
||||||
domainAxis.addDomainValue(domainLowerBound);
|
|
||||||
domainAxis.addDomainValue(domainUpperBound);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void configureMeasureAxes(List<MutableSeries<D>> seriesList) {
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
if (series.data.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final domainAxis = series.getAttr(domainAxisKey);
|
|
||||||
final domainFn = series.domainFn;
|
|
||||||
|
|
||||||
if (domainAxis == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final measureAxis = series.getAttr(measureAxisKey);
|
|
||||||
if (measureAxis == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only add the measure values for datum who's domain is within the
|
|
||||||
// domainAxis viewport.
|
|
||||||
int startIndex =
|
|
||||||
findNearestViewportStart(domainAxis, domainFn, series.data);
|
|
||||||
int endIndex = findNearestViewportEnd(domainAxis, domainFn, series.data);
|
|
||||||
|
|
||||||
addMeasureValuesFor(series, measureAxis, startIndex, endIndex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void addMeasureValuesFor(
|
|
||||||
MutableSeries<D> series, Axis measureAxis, int startIndex, int endIndex) {
|
|
||||||
final measureFn = series.measureFn;
|
|
||||||
final measureOffsetFn = series.measureOffsetFn;
|
|
||||||
final measureLowerBoundFn = series.measureLowerBoundFn;
|
|
||||||
final measureUpperBoundFn = series.measureUpperBoundFn;
|
|
||||||
|
|
||||||
for (int i = startIndex; i <= endIndex; i++) {
|
|
||||||
final measure = measureFn(i);
|
|
||||||
final measureOffset = measureOffsetFn(i);
|
|
||||||
|
|
||||||
if (measure != null && measureOffset != null) {
|
|
||||||
measureAxis.addDomainValue(measure + measureOffset);
|
|
||||||
|
|
||||||
if (measureLowerBoundFn != null && measureUpperBoundFn != null) {
|
|
||||||
measureAxis.addDomainValue(measureLowerBoundFn(i) + measureOffset);
|
|
||||||
measureAxis.addDomainValue(measureUpperBoundFn(i) + measureOffset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
int findNearestViewportStart(
|
|
||||||
Axis domainAxis, AccessorFn<D> domainFn, List data) {
|
|
||||||
if (data.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quick optimization for full viewport (likely).
|
|
||||||
if (domainAxis.compareDomainValueToViewport(domainFn(0)) == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = 1; // Index zero was already checked for above.
|
|
||||||
var end = data.length - 1;
|
|
||||||
|
|
||||||
// Binary search for the start of the viewport.
|
|
||||||
while (end >= start) {
|
|
||||||
int searchIndex = ((end - start) / 2).floor() + start;
|
|
||||||
int prevIndex = searchIndex - 1;
|
|
||||||
|
|
||||||
var comparisonValue =
|
|
||||||
domainAxis.compareDomainValueToViewport(domainFn(searchIndex));
|
|
||||||
var prevComparisonValue =
|
|
||||||
domainAxis.compareDomainValueToViewport(domainFn(prevIndex));
|
|
||||||
|
|
||||||
// Found start?
|
|
||||||
if (prevComparisonValue == -1 && comparisonValue == 0) {
|
|
||||||
return searchIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Straddling viewport?
|
|
||||||
// Return previous index as the nearest start of the viewport.
|
|
||||||
if (comparisonValue == 1 && prevComparisonValue == -1) {
|
|
||||||
return (searchIndex - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Before start? Update startIndex
|
|
||||||
if (comparisonValue == -1) {
|
|
||||||
start = searchIndex + 1;
|
|
||||||
} else {
|
|
||||||
// Middle or after viewport? Update endIndex
|
|
||||||
end = searchIndex - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary search would reach this point for the edge cases where the domain
|
|
||||||
// specified is prior or after the domain viewport.
|
|
||||||
// If domain is prior to the domain viewport, return the first index as the
|
|
||||||
// nearest viewport start.
|
|
||||||
// If domain is after the domain viewport, return the last index as the
|
|
||||||
// nearest viewport start.
|
|
||||||
final lastComparison =
|
|
||||||
domainAxis.compareDomainValueToViewport(domainFn(data.length - 1));
|
|
||||||
return lastComparison == 1 ? (data.length - 1) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
int findNearestViewportEnd(
|
|
||||||
Axis domainAxis, AccessorFn<D> domainFn, List data) {
|
|
||||||
if (data.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var start = 1;
|
|
||||||
var end = data.length - 1;
|
|
||||||
|
|
||||||
// Quick optimization for full viewport (likely).
|
|
||||||
if (domainAxis.compareDomainValueToViewport(domainFn(end)) == 0) {
|
|
||||||
return end;
|
|
||||||
}
|
|
||||||
end = end - 1; // Last index was already checked for above.
|
|
||||||
|
|
||||||
// Binary search for the start of the viewport.
|
|
||||||
while (end >= start) {
|
|
||||||
int searchIndex = ((end - start) / 2).floor() + start;
|
|
||||||
int prevIndex = searchIndex - 1;
|
|
||||||
|
|
||||||
int comparisonValue =
|
|
||||||
domainAxis.compareDomainValueToViewport(domainFn(searchIndex));
|
|
||||||
int prevComparisonValue =
|
|
||||||
domainAxis.compareDomainValueToViewport(domainFn(prevIndex));
|
|
||||||
|
|
||||||
// Found end?
|
|
||||||
if (prevComparisonValue == 0 && comparisonValue == 1) {
|
|
||||||
return prevIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Straddling viewport?
|
|
||||||
// Return the current index as the start of the viewport.
|
|
||||||
if (comparisonValue == 1 && prevComparisonValue == -1) {
|
|
||||||
return searchIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
// After end? Update endIndex
|
|
||||||
if (comparisonValue == 1) {
|
|
||||||
end = searchIndex - 1;
|
|
||||||
} else {
|
|
||||||
// Middle or before viewport? Update startIndex
|
|
||||||
start = searchIndex + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary search would reach this point for the edge cases where the domain
|
|
||||||
// specified is prior or after the domain viewport.
|
|
||||||
// If domain is prior to the domain viewport, return the first index as the
|
|
||||||
// nearest viewport end.
|
|
||||||
// If domain is after the domain viewport, return the last index as the
|
|
||||||
// nearest viewport end.
|
|
||||||
final lastComparison =
|
|
||||||
domainAxis.compareDomainValueToViewport(domainFn(data.length - 1));
|
|
||||||
return lastComparison == 1 ? (data.length - 1) : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,707 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show Rectangle, Point;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show protected;
|
|
||||||
|
|
||||||
import '../../common/gesture_listener.dart' show GestureListener;
|
|
||||||
import '../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../common/proxy_gesture_listener.dart' show ProxyGestureListener;
|
|
||||||
import '../../data/series.dart' show Series;
|
|
||||||
import '../layout/layout_config.dart' show LayoutConfig;
|
|
||||||
import '../layout/layout_manager.dart' show LayoutManager;
|
|
||||||
import '../layout/layout_manager_impl.dart' show LayoutManagerImpl;
|
|
||||||
import '../layout/layout_view.dart' show LayoutView;
|
|
||||||
import 'behavior/chart_behavior.dart' show ChartBehavior;
|
|
||||||
import 'chart_canvas.dart' show ChartCanvas;
|
|
||||||
import 'chart_context.dart' show ChartContext;
|
|
||||||
import 'datum_details.dart' show DatumDetails;
|
|
||||||
import 'processed_series.dart' show MutableSeries;
|
|
||||||
import 'selection_model/selection_model.dart'
|
|
||||||
show MutableSelectionModel, SelectionModelType;
|
|
||||||
import 'series_datum.dart' show SeriesDatum;
|
|
||||||
import 'series_renderer.dart' show SeriesRenderer, rendererIdKey, rendererKey;
|
|
||||||
|
|
||||||
typedef BehaviorCreator = ChartBehavior<D> Function<D>();
|
|
||||||
|
|
||||||
abstract class BaseChart<D> {
|
|
||||||
ChartContext context;
|
|
||||||
|
|
||||||
/// Internal use only.
|
|
||||||
GraphicsFactory graphicsFactory;
|
|
||||||
|
|
||||||
LayoutManager _layoutManager;
|
|
||||||
|
|
||||||
int _chartWidth;
|
|
||||||
int _chartHeight;
|
|
||||||
|
|
||||||
Duration transition = const Duration(milliseconds: 300);
|
|
||||||
double animationPercent;
|
|
||||||
|
|
||||||
bool _animationsTemporarilyDisabled = false;
|
|
||||||
|
|
||||||
/// List of series that were passed into the previous draw call.
|
|
||||||
///
|
|
||||||
/// This list will be used when redraw is called, to reset the state of all
|
|
||||||
/// behaviors to the original list.
|
|
||||||
List<MutableSeries<D>> _originalSeriesList;
|
|
||||||
|
|
||||||
/// List of series that are currently drawn on the chart.
|
|
||||||
///
|
|
||||||
/// This list should be used by interactive behaviors between chart draw
|
|
||||||
/// cycles. It may be filtered or modified by some behaviors during the
|
|
||||||
/// initial draw cycle (e.g. a [Legend] may hide some series).
|
|
||||||
List<MutableSeries<D>> _currentSeriesList;
|
|
||||||
|
|
||||||
Set<String> _usingRenderers = Set<String>();
|
|
||||||
Map<String, List<MutableSeries<D>>> _rendererToSeriesList;
|
|
||||||
|
|
||||||
final _seriesRenderers = <String, SeriesRenderer<D>>{};
|
|
||||||
|
|
||||||
/// Map of named chart behaviors attached to this chart.
|
|
||||||
final _behaviorRoleMap = <String, ChartBehavior<D>>{};
|
|
||||||
final _behaviorStack = <ChartBehavior<D>>[];
|
|
||||||
|
|
||||||
final _behaviorTappableMap = <String, ChartBehavior<D>>{};
|
|
||||||
|
|
||||||
/// Whether or not the chart will respond to tap events.
|
|
||||||
///
|
|
||||||
/// This will generally be true if there is a behavior attached to the chart
|
|
||||||
/// that does something with tap events, such as "click to select data."
|
|
||||||
bool get isTappable => _behaviorTappableMap.isNotEmpty;
|
|
||||||
|
|
||||||
final _gestureProxy = ProxyGestureListener();
|
|
||||||
|
|
||||||
final _selectionModels = <SelectionModelType, MutableSelectionModel<D>>{};
|
|
||||||
|
|
||||||
/// Whether data should be selected by nearest domain distance, or by relative
|
|
||||||
/// distance.
|
|
||||||
///
|
|
||||||
/// This should generally be true for chart types that are intended to be
|
|
||||||
/// aggregated by domain, and false for charts that plot arbitrary x,y data.
|
|
||||||
/// Scatter plots, for example, may have many overlapping data with the same
|
|
||||||
/// domain value.
|
|
||||||
bool get selectNearestByDomain => true;
|
|
||||||
|
|
||||||
final _lifecycleListeners = <LifecycleListener<D>>[];
|
|
||||||
|
|
||||||
BaseChart({LayoutConfig layoutConfig}) {
|
|
||||||
_layoutManager = LayoutManagerImpl(config: layoutConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
void init(ChartContext context, GraphicsFactory graphicsFactory) {
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
// When graphics factory is updated, update all the views.
|
|
||||||
if (this.graphicsFactory != graphicsFactory) {
|
|
||||||
this.graphicsFactory = graphicsFactory;
|
|
||||||
|
|
||||||
_layoutManager
|
|
||||||
.applyToViews((view) => view.graphicsFactory = graphicsFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
configurationChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finish configuring components that require context and graphics factory.
|
|
||||||
///
|
|
||||||
/// Some components require context and graphics factory to be set again when
|
|
||||||
/// configuration has changed but the configuration is set prior to the
|
|
||||||
/// chart first calling init with the context.
|
|
||||||
void configurationChanged() {}
|
|
||||||
|
|
||||||
int get chartWidth => _chartWidth;
|
|
||||||
|
|
||||||
int get chartHeight => _chartHeight;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Gesture proxy methods
|
|
||||||
//
|
|
||||||
ProxyGestureListener get gestureProxy => _gestureProxy;
|
|
||||||
|
|
||||||
/// Add a [GestureListener] to this chart.
|
|
||||||
GestureListener addGestureListener(GestureListener listener) {
|
|
||||||
_gestureProxy.add(listener);
|
|
||||||
return listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a [GestureListener] from this chart.
|
|
||||||
void removeGestureListener(GestureListener listener) {
|
|
||||||
_gestureProxy.remove(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
LifecycleListener addLifecycleListener(LifecycleListener<D> listener) {
|
|
||||||
_lifecycleListeners.add(listener);
|
|
||||||
return listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool removeLifecycleListener(LifecycleListener<D> listener) =>
|
|
||||||
_lifecycleListeners.remove(listener);
|
|
||||||
|
|
||||||
/// Returns MutableSelectionModel for the given type. Lazy creates one upon first
|
|
||||||
/// request.
|
|
||||||
MutableSelectionModel<D> getSelectionModel(SelectionModelType type) {
|
|
||||||
return _selectionModels.putIfAbsent(type, () => MutableSelectionModel<D>());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a list of datum details from selection model of [type].
|
|
||||||
List<DatumDetails<D>> getDatumDetails(SelectionModelType type);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Renderer methods
|
|
||||||
//
|
|
||||||
|
|
||||||
set defaultRenderer(SeriesRenderer<D> renderer) {
|
|
||||||
renderer.rendererId = SeriesRenderer.defaultRendererId;
|
|
||||||
addSeriesRenderer(renderer);
|
|
||||||
}
|
|
||||||
|
|
||||||
SeriesRenderer<D> get defaultRenderer =>
|
|
||||||
getSeriesRenderer(SeriesRenderer.defaultRendererId);
|
|
||||||
|
|
||||||
void addSeriesRenderer(SeriesRenderer renderer) {
|
|
||||||
String rendererId = renderer.rendererId;
|
|
||||||
|
|
||||||
SeriesRenderer<D> previousRenderer = _seriesRenderers[rendererId];
|
|
||||||
if (previousRenderer != null) {
|
|
||||||
removeView(previousRenderer);
|
|
||||||
previousRenderer.onDetach(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
addView(renderer);
|
|
||||||
renderer.onAttach(this);
|
|
||||||
_seriesRenderers[rendererId] = renderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
SeriesRenderer<D> getSeriesRenderer(String rendererId) {
|
|
||||||
SeriesRenderer<D> renderer = _seriesRenderers[rendererId];
|
|
||||||
|
|
||||||
// Special case, if we are asking for the default and we haven't made it
|
|
||||||
// yet, then make it now.
|
|
||||||
if (renderer == null) {
|
|
||||||
if (rendererId == SeriesRenderer.defaultRendererId) {
|
|
||||||
renderer = makeDefaultRenderer();
|
|
||||||
defaultRenderer = renderer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: throw error if couldn't find renderer by id?
|
|
||||||
|
|
||||||
return renderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
SeriesRenderer<D> makeDefaultRenderer();
|
|
||||||
|
|
||||||
bool pointWithinRenderer(Point<double> chartPosition) {
|
|
||||||
return _usingRenderers.any((rendererId) => getSeriesRenderer(rendererId)
|
|
||||||
.componentBounds
|
|
||||||
.containsPoint(chartPosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieves the datum details that are nearest to the given [drawAreaPoint].
|
|
||||||
///
|
|
||||||
/// [drawAreaPoint] represents a point in the chart, such as a point that was
|
|
||||||
/// clicked/tapped on by a user.
|
|
||||||
///
|
|
||||||
/// [selectAcrossAllDrawAreaComponents] specifies whether nearest data
|
|
||||||
/// selection should be done across the combined draw area of all components
|
|
||||||
/// with series draw areas, or just the chart's primary draw area bounds.
|
|
||||||
List<DatumDetails<D>> getNearestDatumDetailPerSeries(
|
|
||||||
Point<double> drawAreaPoint, bool selectAcrossAllDrawAreaComponents) {
|
|
||||||
// Optionally grab the combined draw area bounds of all components. If this
|
|
||||||
// is disabled, then we expect each series renderer to filter out the event
|
|
||||||
// if [chartPoint] is located outside of its own component bounds.
|
|
||||||
final boundsOverride =
|
|
||||||
selectAcrossAllDrawAreaComponents ? drawableLayoutAreaBounds : null;
|
|
||||||
|
|
||||||
final details = <DatumDetails<D>>[];
|
|
||||||
_usingRenderers.forEach((rendererId) {
|
|
||||||
details.addAll(getSeriesRenderer(rendererId)
|
|
||||||
.getNearestDatumDetailPerSeries(
|
|
||||||
drawAreaPoint, selectNearestByDomain, boundsOverride));
|
|
||||||
});
|
|
||||||
|
|
||||||
details.sort((a, b) {
|
|
||||||
// Sort so that the nearest one is first.
|
|
||||||
// Special sort, sort by domain distance first, then by measure distance.
|
|
||||||
if (selectNearestByDomain) {
|
|
||||||
int domainDiff = a.domainDistance.compareTo(b.domainDistance);
|
|
||||||
if (domainDiff == 0) {
|
|
||||||
return a.measureDistance.compareTo(b.measureDistance);
|
|
||||||
}
|
|
||||||
return domainDiff;
|
|
||||||
} else {
|
|
||||||
return a.relativeDistance.compareTo(b.relativeDistance);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieves the datum details for the current chart selection.
|
|
||||||
///
|
|
||||||
/// [selectionModelType] specifies the type of the selection model to use.
|
|
||||||
List<DatumDetails<D>> getSelectedDatumDetails(
|
|
||||||
SelectionModelType selectionModelType) {
|
|
||||||
final details = <DatumDetails<D>>[];
|
|
||||||
|
|
||||||
if (_currentSeriesList == null) {
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
final selectionModel = getSelectionModel(selectionModelType);
|
|
||||||
if (selectionModel == null || !selectionModel.hasDatumSelection) {
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass each selected datum to the appropriate series renderer to get full
|
|
||||||
// details appropriate to its series type.
|
|
||||||
for (SeriesDatum<D> seriesDatum in selectionModel.selectedDatum) {
|
|
||||||
final rendererId = seriesDatum.series.getAttr(rendererIdKey);
|
|
||||||
details.add(
|
|
||||||
getSeriesRenderer(rendererId).getDetailsForSeriesDatum(seriesDatum));
|
|
||||||
}
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Behavior methods
|
|
||||||
//
|
|
||||||
|
|
||||||
/// Helper method to create a behavior with congruent types.
|
|
||||||
///
|
|
||||||
/// This invokes the provides helper with type parameters that match this
|
|
||||||
/// chart.
|
|
||||||
ChartBehavior<D> createBehavior(BehaviorCreator creator) => creator<D>();
|
|
||||||
|
|
||||||
/// Attaches a behavior to the chart.
|
|
||||||
///
|
|
||||||
/// Setting a new behavior with the same role as a behavior already attached
|
|
||||||
/// to the chart will replace the old behavior. The old behavior's removeFrom
|
|
||||||
/// method will be called before we attach the new behavior.
|
|
||||||
void addBehavior(ChartBehavior<D> behavior) {
|
|
||||||
final role = behavior.role;
|
|
||||||
|
|
||||||
if (role != null && _behaviorRoleMap[role] != behavior) {
|
|
||||||
// Remove any old behavior with the same role.
|
|
||||||
removeBehavior(_behaviorRoleMap[role]);
|
|
||||||
// Add the new behavior.
|
|
||||||
_behaviorRoleMap[role] = behavior;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the behavior if it wasn't already added.
|
|
||||||
if (!_behaviorStack.contains(behavior)) {
|
|
||||||
_behaviorStack.add(behavior);
|
|
||||||
behavior.attachTo(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes a behavior from the chart.
|
|
||||||
///
|
|
||||||
/// Returns true if a behavior was removed, otherwise returns false.
|
|
||||||
bool removeBehavior(ChartBehavior<D> behavior) {
|
|
||||||
if (behavior == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final role = behavior?.role;
|
|
||||||
if (role != null && _behaviorRoleMap[role] == behavior) {
|
|
||||||
_behaviorRoleMap.remove(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the removed behavior is no longer registered for tap events.
|
|
||||||
unregisterTappable(behavior);
|
|
||||||
|
|
||||||
final wasAttached = _behaviorStack.remove(behavior);
|
|
||||||
behavior.removeFrom(this);
|
|
||||||
|
|
||||||
return wasAttached;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tells the chart that this behavior responds to tap events.
|
|
||||||
///
|
|
||||||
/// This should only be called after [behavior] has been attached to the chart
|
|
||||||
/// via [addBehavior].
|
|
||||||
void registerTappable(ChartBehavior<D> behavior) {
|
|
||||||
final role = behavior.role;
|
|
||||||
|
|
||||||
if (role != null &&
|
|
||||||
_behaviorRoleMap[role] == behavior &&
|
|
||||||
_behaviorTappableMap[role] != behavior) {
|
|
||||||
_behaviorTappableMap[role] = behavior;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tells the chart that this behavior no longer responds to tap events.
|
|
||||||
void unregisterTappable(ChartBehavior<D> behavior) {
|
|
||||||
final role = behavior?.role;
|
|
||||||
if (role != null && _behaviorTappableMap[role] == behavior) {
|
|
||||||
_behaviorTappableMap.remove(role);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a list of behaviors that have been added.
|
|
||||||
List<ChartBehavior<D>> get behaviors => List.unmodifiable(_behaviorStack);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Layout methods
|
|
||||||
//
|
|
||||||
void measure(int width, int height) {
|
|
||||||
if (_rendererToSeriesList != null) {
|
|
||||||
_layoutManager.measure(width, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void layout(int width, int height) {
|
|
||||||
if (_rendererToSeriesList != null) {
|
|
||||||
layoutInternal(width, height);
|
|
||||||
|
|
||||||
onPostLayout(_rendererToSeriesList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void layoutInternal(int width, int height) {
|
|
||||||
_chartWidth = width;
|
|
||||||
_chartHeight = height;
|
|
||||||
_layoutManager.layout(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addView(LayoutView view) {
|
|
||||||
if (_layoutManager.isAttached(view) == false) {
|
|
||||||
view.graphicsFactory = graphicsFactory;
|
|
||||||
_layoutManager.addView(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeView(LayoutView view) {
|
|
||||||
_layoutManager.removeView(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether or not [point] is within the draw area bounds.
|
|
||||||
bool withinDrawArea(Point<num> point) {
|
|
||||||
return _layoutManager.withinDrawArea(point);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the bounds of the chart draw area.
|
|
||||||
Rectangle<int> get drawAreaBounds => _layoutManager.drawAreaBounds;
|
|
||||||
|
|
||||||
int get marginBottom => _layoutManager.marginBottom;
|
|
||||||
|
|
||||||
int get marginLeft => _layoutManager.marginLeft;
|
|
||||||
|
|
||||||
int get marginRight => _layoutManager.marginRight;
|
|
||||||
|
|
||||||
int get marginTop => _layoutManager.marginTop;
|
|
||||||
|
|
||||||
/// Returns the combined bounds of the chart draw area and all layout
|
|
||||||
/// components that draw series data.
|
|
||||||
Rectangle<int> get drawableLayoutAreaBounds =>
|
|
||||||
_layoutManager.drawableLayoutAreaBounds;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Draw methods
|
|
||||||
//
|
|
||||||
void draw(List<Series<dynamic, D>> seriesList) {
|
|
||||||
// Clear the selection model when [seriesList] changes.
|
|
||||||
for (final selectionModel in _selectionModels.values) {
|
|
||||||
selectionModel.clearSelection(notifyListeners: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var processedSeriesList =
|
|
||||||
List<MutableSeries<D>>.from(seriesList.map(makeSeries));
|
|
||||||
|
|
||||||
// Allow listeners to manipulate the seriesList.
|
|
||||||
fireOnDraw(processedSeriesList);
|
|
||||||
|
|
||||||
// Set an index on the series list.
|
|
||||||
// This can be used by listeners of selection to determine the order of
|
|
||||||
// series, because the selection details are not returned in this order.
|
|
||||||
int seriesIndex = 0;
|
|
||||||
processedSeriesList.forEach((series) => series.seriesIndex = seriesIndex++);
|
|
||||||
|
|
||||||
// Initially save a reference to processedSeriesList. After drawInternal
|
|
||||||
// finishes, we expect _currentSeriesList to contain a new, possibly
|
|
||||||
// modified list.
|
|
||||||
_currentSeriesList = processedSeriesList;
|
|
||||||
|
|
||||||
// Store off processedSeriesList for use later during redraw calls. This
|
|
||||||
// list will not reflect any modifications that were made to
|
|
||||||
// _currentSeriesList by behaviors during the draw cycle.
|
|
||||||
_originalSeriesList = processedSeriesList;
|
|
||||||
|
|
||||||
drawInternal(processedSeriesList, skipAnimation: false, skipLayout: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Redraws and re-lays-out the chart using the previously rendered layout
|
|
||||||
/// dimensions.
|
|
||||||
void redraw({bool skipAnimation = false, bool skipLayout = false}) {
|
|
||||||
drawInternal(_originalSeriesList,
|
|
||||||
skipAnimation: skipAnimation, skipLayout: skipLayout);
|
|
||||||
|
|
||||||
// Trigger layout and actually redraw the chart.
|
|
||||||
if (!skipLayout) {
|
|
||||||
measure(_chartWidth, _chartHeight);
|
|
||||||
layout(_chartWidth, _chartHeight);
|
|
||||||
} else {
|
|
||||||
onSkipLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawInternal(List<MutableSeries<D>> seriesList,
|
|
||||||
{bool skipAnimation, bool skipLayout}) {
|
|
||||||
seriesList =
|
|
||||||
seriesList.map((series) => MutableSeries<D>.clone(series)).toList();
|
|
||||||
|
|
||||||
// TODO: Handle exiting renderers.
|
|
||||||
_animationsTemporarilyDisabled = skipAnimation;
|
|
||||||
|
|
||||||
configureSeries(seriesList);
|
|
||||||
|
|
||||||
// Allow listeners to manipulate the processed seriesList.
|
|
||||||
fireOnPreprocess(seriesList);
|
|
||||||
|
|
||||||
_rendererToSeriesList = preprocessSeries(seriesList);
|
|
||||||
|
|
||||||
// Allow listeners to manipulate the processed seriesList.
|
|
||||||
fireOnPostprocess(seriesList);
|
|
||||||
|
|
||||||
_currentSeriesList = seriesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<MutableSeries<D>> get currentSeriesList => _currentSeriesList;
|
|
||||||
|
|
||||||
MutableSeries<D> makeSeries(Series<dynamic, D> series) {
|
|
||||||
final s = MutableSeries<D>(series);
|
|
||||||
|
|
||||||
// Setup the Renderer
|
|
||||||
final rendererId =
|
|
||||||
series.getAttribute(rendererIdKey) ?? SeriesRenderer.defaultRendererId;
|
|
||||||
s.setAttr(rendererIdKey, rendererId);
|
|
||||||
s.setAttr(rendererKey, getSeriesRenderer(rendererId));
|
|
||||||
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Preprocess series to assign missing color functions.
|
|
||||||
void configureSeries(List<MutableSeries<D>> seriesList) {
|
|
||||||
Map<String, List<MutableSeries<D>>> rendererToSeriesList = {};
|
|
||||||
|
|
||||||
// Build map of rendererIds to SeriesLists. This map can't be re-used later
|
|
||||||
// in the preprocessSeries call because some behaviors might alter the
|
|
||||||
// seriesList.
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
String rendererId = series.getAttr(rendererIdKey);
|
|
||||||
rendererToSeriesList.putIfAbsent(rendererId, () => []).add(series);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Have each renderer add missing color functions to their seriesLists.
|
|
||||||
rendererToSeriesList.forEach((rendererId, seriesList) {
|
|
||||||
getSeriesRenderer(rendererId).configureSeries(seriesList);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Preprocess series to allow stacking and other mutations.
|
|
||||||
///
|
|
||||||
/// Build a map of rendererId to series.
|
|
||||||
Map<String, List<MutableSeries<D>>> preprocessSeries(
|
|
||||||
List<MutableSeries<D>> seriesList) {
|
|
||||||
Map<String, List<MutableSeries<D>>> rendererToSeriesList = {};
|
|
||||||
|
|
||||||
var unusedRenderers = _usingRenderers;
|
|
||||||
_usingRenderers = Set<String>();
|
|
||||||
|
|
||||||
// Build map of rendererIds to SeriesLists.
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
String rendererId = series.getAttr(rendererIdKey);
|
|
||||||
rendererToSeriesList.putIfAbsent(rendererId, () => []).add(series);
|
|
||||||
|
|
||||||
_usingRenderers.add(rendererId);
|
|
||||||
unusedRenderers.remove(rendererId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Allow unused renderers to render out content.
|
|
||||||
unusedRenderers
|
|
||||||
.forEach((rendererId) => rendererToSeriesList[rendererId] = []);
|
|
||||||
|
|
||||||
// Have each renderer preprocess their seriesLists.
|
|
||||||
rendererToSeriesList.forEach((rendererId, seriesList) {
|
|
||||||
getSeriesRenderer(rendererId).preprocessSeries(seriesList);
|
|
||||||
});
|
|
||||||
|
|
||||||
return rendererToSeriesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onSkipLayout() {
|
|
||||||
onPostLayout(_rendererToSeriesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPostLayout(Map<String, List<MutableSeries<D>>> rendererToSeriesList) {
|
|
||||||
// Update each renderer with
|
|
||||||
rendererToSeriesList.forEach((rendererId, seriesList) {
|
|
||||||
getSeriesRenderer(rendererId).update(seriesList, animatingThisDraw);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Request animation
|
|
||||||
if (animatingThisDraw) {
|
|
||||||
animationPercent = 0.0;
|
|
||||||
context.requestAnimation(this.transition);
|
|
||||||
} else {
|
|
||||||
animationPercent = 1.0;
|
|
||||||
context.requestPaint();
|
|
||||||
}
|
|
||||||
|
|
||||||
_animationsTemporarilyDisabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void paint(ChartCanvas canvas) {
|
|
||||||
canvas.drawingView = 'BaseView';
|
|
||||||
_layoutManager.paintOrderedViews.forEach((view) {
|
|
||||||
canvas.drawingView = view.runtimeType.toString();
|
|
||||||
view.paint(canvas, animatingThisDraw ? animationPercent : 1.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.drawingView = 'PostRender';
|
|
||||||
fireOnPostrender(canvas);
|
|
||||||
canvas.drawingView = null;
|
|
||||||
|
|
||||||
if (animationPercent == 1.0) {
|
|
||||||
fireOnAnimationComplete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get animatingThisDraw => (transition != null &&
|
|
||||||
transition.inMilliseconds > 0 &&
|
|
||||||
!_animationsTemporarilyDisabled);
|
|
||||||
|
|
||||||
@protected
|
|
||||||
fireOnDraw(List<MutableSeries<D>> seriesList) {
|
|
||||||
_lifecycleListeners.forEach((listener) {
|
|
||||||
if (listener.onData != null) {
|
|
||||||
listener.onData(seriesList);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
fireOnPreprocess(List<MutableSeries<D>> seriesList) {
|
|
||||||
_lifecycleListeners.forEach((listener) {
|
|
||||||
if (listener.onPreprocess != null) {
|
|
||||||
listener.onPreprocess(seriesList);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
fireOnPostprocess(List<MutableSeries<D>> seriesList) {
|
|
||||||
_lifecycleListeners.forEach((listener) {
|
|
||||||
if (listener.onPostprocess != null) {
|
|
||||||
listener.onPostprocess(seriesList);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
fireOnAxisConfigured() {
|
|
||||||
_lifecycleListeners.forEach((listener) {
|
|
||||||
if (listener.onAxisConfigured != null) {
|
|
||||||
listener.onAxisConfigured();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
fireOnPostrender(ChartCanvas canvas) {
|
|
||||||
_lifecycleListeners.forEach((listener) {
|
|
||||||
if (listener.onPostrender != null) {
|
|
||||||
listener.onPostrender(canvas);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
fireOnAnimationComplete() {
|
|
||||||
_lifecycleListeners.forEach((listener) {
|
|
||||||
if (listener.onAnimationComplete != null) {
|
|
||||||
listener.onAnimationComplete();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called to free up any resources due to chart going away.
|
|
||||||
destroy() {
|
|
||||||
// Walk them in add order to support behaviors that remove other behaviors.
|
|
||||||
for (var i = 0; i < _behaviorStack.length; i++) {
|
|
||||||
_behaviorStack[i].removeFrom(this);
|
|
||||||
}
|
|
||||||
_behaviorStack.clear();
|
|
||||||
_behaviorRoleMap.clear();
|
|
||||||
_selectionModels.values
|
|
||||||
.forEach((selectionModel) => selectionModel.clearAllListeners());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LifecycleListener<D> {
|
|
||||||
/// Called when new data is drawn to the chart (not a redraw).
|
|
||||||
///
|
|
||||||
/// This step is good for processing the data (running averages, percentage of
|
|
||||||
/// first, etc). It can also be used to add Series of data (trend line) or
|
|
||||||
/// remove a line as mentioned above, removing Series.
|
|
||||||
final LifecycleSeriesListCallback onData;
|
|
||||||
|
|
||||||
/// Called for every redraw given the original SeriesList resulting from the
|
|
||||||
/// previous onData.
|
|
||||||
///
|
|
||||||
/// This step is good for injecting default attributes on the Series before
|
|
||||||
/// the renderers process the data (ex: before stacking measures).
|
|
||||||
final LifecycleSeriesListCallback onPreprocess;
|
|
||||||
|
|
||||||
/// Called after the chart and renderers get a chance to process the data but
|
|
||||||
/// before the axes process them.
|
|
||||||
///
|
|
||||||
/// This step is good if you need to alter the Series measure values after the
|
|
||||||
/// renderers have processed them (ex: after stacking measures).
|
|
||||||
final LifecycleSeriesListCallback onPostprocess;
|
|
||||||
|
|
||||||
/// Called after the Axes have been configured.
|
|
||||||
/// This step is good if you need to use the axes to get any cartesian
|
|
||||||
/// location information. At this point Axes should be immutable and stable.
|
|
||||||
final LifecycleEmptyCallback onAxisConfigured;
|
|
||||||
|
|
||||||
/// Called after the chart is done rendering passing along the canvas allowing
|
|
||||||
/// a behavior or other listener to render on top of the chart.
|
|
||||||
///
|
|
||||||
/// This is a convenience callback, however if there is any significant canvas
|
|
||||||
/// interaction or stacking needs, it is preferred that a AplosView/ChartView
|
|
||||||
/// is added to the chart instead to fully participate in the view stacking.
|
|
||||||
final LifecycleCanvasCallback onPostrender;
|
|
||||||
|
|
||||||
/// Called after animation hits 100%. This allows a behavior or other listener
|
|
||||||
/// to chain animations to create a multiple step animation transition.
|
|
||||||
final LifecycleEmptyCallback onAnimationComplete;
|
|
||||||
|
|
||||||
LifecycleListener(
|
|
||||||
{this.onData,
|
|
||||||
this.onPreprocess,
|
|
||||||
this.onPostprocess,
|
|
||||||
this.onAxisConfigured,
|
|
||||||
this.onPostrender,
|
|
||||||
this.onAnimationComplete});
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef LifecycleSeriesListCallback<D> = Function(
|
|
||||||
List<MutableSeries<D>> seriesList);
|
|
||||||
typedef LifecycleCanvasCallback = Function(ChartCanvas canvas);
|
|
||||||
typedef LifecycleEmptyCallback = Function();
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/gesture_listener.dart' show GestureListener;
|
|
||||||
import '../../base_chart.dart' show BaseChart;
|
|
||||||
import '../chart_behavior.dart' show ChartBehavior;
|
|
||||||
import 'a11y_node.dart' show A11yNode;
|
|
||||||
|
|
||||||
/// The gesture to use for triggering explore mode.
|
|
||||||
enum ExploreModeTrigger {
|
|
||||||
pressHold,
|
|
||||||
tap,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Chart behavior for adding A11y information.
|
|
||||||
abstract class A11yExploreBehavior<D> implements ChartBehavior<D> {
|
|
||||||
/// The gesture that activates explore mode. Defaults to long press.
|
|
||||||
///
|
|
||||||
/// Turning on explore mode asks this [A11yExploreBehavior] to generate nodes within
|
|
||||||
/// this chart.
|
|
||||||
final ExploreModeTrigger exploreModeTrigger;
|
|
||||||
|
|
||||||
/// Minimum width of the bounding box for the a11y focus.
|
|
||||||
///
|
|
||||||
/// Must be 1 or higher because invisible semantic nodes should not be added.
|
|
||||||
final double minimumWidth;
|
|
||||||
|
|
||||||
/// Optionally notify the OS when explore mode is enabled.
|
|
||||||
final String exploreModeEnabledAnnouncement;
|
|
||||||
|
|
||||||
/// Optionally notify the OS when explore mode is disabled.
|
|
||||||
final String exploreModeDisabledAnnouncement;
|
|
||||||
|
|
||||||
BaseChart<D> _chart;
|
|
||||||
GestureListener _listener;
|
|
||||||
bool _exploreModeOn = false;
|
|
||||||
|
|
||||||
A11yExploreBehavior({
|
|
||||||
this.exploreModeTrigger = ExploreModeTrigger.pressHold,
|
|
||||||
double minimumWidth,
|
|
||||||
this.exploreModeEnabledAnnouncement,
|
|
||||||
this.exploreModeDisabledAnnouncement,
|
|
||||||
}) : minimumWidth = minimumWidth ?? 1.0 {
|
|
||||||
assert(this.minimumWidth >= 1.0);
|
|
||||||
|
|
||||||
switch (exploreModeTrigger) {
|
|
||||||
case ExploreModeTrigger.pressHold:
|
|
||||||
_listener = GestureListener(onLongPress: _toggleExploreMode);
|
|
||||||
break;
|
|
||||||
case ExploreModeTrigger.tap:
|
|
||||||
_listener = GestureListener(onTap: _toggleExploreMode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _toggleExploreMode(_) {
|
|
||||||
if (_exploreModeOn) {
|
|
||||||
_exploreModeOn = false;
|
|
||||||
// Ask native platform to turn off explore mode.
|
|
||||||
_chart.context.disableA11yExploreMode(
|
|
||||||
announcement: exploreModeDisabledAnnouncement);
|
|
||||||
} else {
|
|
||||||
_exploreModeOn = true;
|
|
||||||
// Ask native platform to turn on explore mode.
|
|
||||||
_chart.context.enableA11yExploreMode(createA11yNodes(),
|
|
||||||
announcement: exploreModeEnabledAnnouncement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a list of A11yNodes for this chart.
|
|
||||||
List<A11yNode> createA11yNodes();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
_chart = chart;
|
|
||||||
chart.addGestureListener(_listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart<D> chart) {
|
|
||||||
chart.removeGestureListener(_listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show Rectangle;
|
|
||||||
|
|
||||||
typedef OnFocus = void Function();
|
|
||||||
|
|
||||||
/// Container for accessibility data.
|
|
||||||
class A11yNode {
|
|
||||||
/// The bounding box for this node.
|
|
||||||
final Rectangle<int> boundingBox;
|
|
||||||
|
|
||||||
/// The textual description of this node.
|
|
||||||
final String label;
|
|
||||||
|
|
||||||
/// Callback when the A11yNode is focused by the native platform
|
|
||||||
OnFocus onFocus;
|
|
||||||
|
|
||||||
A11yNode(this.label, this.boundingBox, {this.onFocus});
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show Rectangle;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show required;
|
|
||||||
|
|
||||||
import '../../../cartesian/axis/axis.dart' show ImmutableAxis, domainAxisKey;
|
|
||||||
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
|
|
||||||
import '../../base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
import '../../processed_series.dart' show MutableSeries;
|
|
||||||
import '../../selection_model/selection_model.dart' show SelectionModelType;
|
|
||||||
import '../../series_datum.dart' show SeriesDatum;
|
|
||||||
import 'a11y_explore_behavior.dart'
|
|
||||||
show A11yExploreBehavior, ExploreModeTrigger;
|
|
||||||
import 'a11y_node.dart' show A11yNode, OnFocus;
|
|
||||||
|
|
||||||
/// Returns a string for a11y vocalization from a list of series datum.
|
|
||||||
typedef VocalizationCallback<D> = String Function(
|
|
||||||
List<SeriesDatum<D>> seriesDatums);
|
|
||||||
|
|
||||||
/// A simple vocalization that returns the domain value to string.
|
|
||||||
String domainVocalization<D>(List<SeriesDatum<D>> seriesDatums) {
|
|
||||||
final datumIndex = seriesDatums.first.index;
|
|
||||||
final domainFn = seriesDatums.first.series.domainFn;
|
|
||||||
final domain = domainFn(datumIndex);
|
|
||||||
|
|
||||||
return domain.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Behavior that generates semantic nodes for each domain.
|
|
||||||
class DomainA11yExploreBehavior<D> extends A11yExploreBehavior<D> {
|
|
||||||
final VocalizationCallback _vocalizationCallback;
|
|
||||||
LifecycleListener<D> _lifecycleListener;
|
|
||||||
CartesianChart<D> _chart;
|
|
||||||
List<MutableSeries<D>> _seriesList;
|
|
||||||
|
|
||||||
DomainA11yExploreBehavior(
|
|
||||||
{VocalizationCallback vocalizationCallback,
|
|
||||||
ExploreModeTrigger exploreModeTrigger,
|
|
||||||
double minimumWidth,
|
|
||||||
String exploreModeEnabledAnnouncement,
|
|
||||||
String exploreModeDisabledAnnouncement})
|
|
||||||
: _vocalizationCallback = vocalizationCallback ?? domainVocalization,
|
|
||||||
super(
|
|
||||||
exploreModeTrigger: exploreModeTrigger,
|
|
||||||
minimumWidth: minimumWidth,
|
|
||||||
exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement,
|
|
||||||
exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement) {
|
|
||||||
_lifecycleListener = LifecycleListener<D>(onPostprocess: _updateSeriesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<A11yNode> createA11yNodes() {
|
|
||||||
final nodes = <_DomainA11yNode>[];
|
|
||||||
|
|
||||||
// Update the selection model when the a11y node has focus.
|
|
||||||
final selectionModel = _chart.getSelectionModel(SelectionModelType.info);
|
|
||||||
|
|
||||||
final domainSeriesDatum = <D, List<SeriesDatum<D>>>{};
|
|
||||||
|
|
||||||
for (MutableSeries<D> series in _seriesList) {
|
|
||||||
for (var index = 0; index < series.data.length; index++) {
|
|
||||||
final datum = series.data[index];
|
|
||||||
D domain = series.domainFn(index);
|
|
||||||
|
|
||||||
domainSeriesDatum[domain] ??= <SeriesDatum<D>>[];
|
|
||||||
domainSeriesDatum[domain].add(SeriesDatum<D>(series, datum));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
domainSeriesDatum.forEach((domain, seriesDatums) {
|
|
||||||
final a11yDescription = _vocalizationCallback(seriesDatums);
|
|
||||||
|
|
||||||
final firstSeries = seriesDatums.first.series;
|
|
||||||
final domainAxis = firstSeries.getAttr(domainAxisKey) as ImmutableAxis<D>;
|
|
||||||
final location = domainAxis.getLocation(domain);
|
|
||||||
|
|
||||||
/// If the step size is smaller than the minimum width, use minimum.
|
|
||||||
final stepSize = (domainAxis.stepSize > minimumWidth)
|
|
||||||
? domainAxis.stepSize
|
|
||||||
: minimumWidth;
|
|
||||||
|
|
||||||
nodes.add(_DomainA11yNode(a11yDescription,
|
|
||||||
location: location,
|
|
||||||
stepSize: stepSize,
|
|
||||||
chartDrawBounds: _chart.drawAreaBounds,
|
|
||||||
isRtl: _chart.context.isRtl,
|
|
||||||
renderVertically: _chart.vertical,
|
|
||||||
onFocus: () => selectionModel.updateSelection(seriesDatums, [])));
|
|
||||||
});
|
|
||||||
|
|
||||||
// The screen reader navigates the nodes based on the order it is returned.
|
|
||||||
// So if the chart is RTL, then the nodes should be ordered with the right
|
|
||||||
// most domain first.
|
|
||||||
//
|
|
||||||
// If the chart has multiple series and one series is missing the domain
|
|
||||||
// and it was added later, we still want the domains to be in order.
|
|
||||||
nodes.sort();
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateSeriesList(List<MutableSeries<D>> seriesList) {
|
|
||||||
_seriesList = seriesList;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
// Domain selection behavior only works for cartesian charts.
|
|
||||||
assert(chart is CartesianChart);
|
|
||||||
_chart = chart as CartesianChart;
|
|
||||||
|
|
||||||
chart.addLifecycleListener(_lifecycleListener);
|
|
||||||
|
|
||||||
super.attachTo(chart);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart chart) {
|
|
||||||
chart.removeLifecycleListener(_lifecycleListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'DomainA11yExplore-$exploreModeTrigger';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A11yNode with domain specific information.
|
|
||||||
class _DomainA11yNode extends A11yNode implements Comparable<_DomainA11yNode> {
|
|
||||||
// Save location, RTL, and is render vertically for sorting
|
|
||||||
final double location;
|
|
||||||
final bool isRtl;
|
|
||||||
final bool renderVertically;
|
|
||||||
|
|
||||||
factory _DomainA11yNode(String label,
|
|
||||||
{@required double location,
|
|
||||||
@required double stepSize,
|
|
||||||
@required Rectangle<int> chartDrawBounds,
|
|
||||||
@required bool isRtl,
|
|
||||||
@required bool renderVertically,
|
|
||||||
OnFocus onFocus}) {
|
|
||||||
Rectangle<int> boundingBox;
|
|
||||||
if (renderVertically) {
|
|
||||||
var left = (location - stepSize / 2).round();
|
|
||||||
var top = chartDrawBounds.top;
|
|
||||||
var width = stepSize.round();
|
|
||||||
var height = chartDrawBounds.height;
|
|
||||||
boundingBox = Rectangle(left, top, width, height);
|
|
||||||
} else {
|
|
||||||
var left = chartDrawBounds.left;
|
|
||||||
var top = (location - stepSize / 2).round();
|
|
||||||
var width = chartDrawBounds.width;
|
|
||||||
var height = stepSize.round();
|
|
||||||
boundingBox = Rectangle(left, top, width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _DomainA11yNode._internal(label, boundingBox,
|
|
||||||
location: location,
|
|
||||||
isRtl: isRtl,
|
|
||||||
renderVertically: renderVertically,
|
|
||||||
onFocus: onFocus);
|
|
||||||
}
|
|
||||||
|
|
||||||
_DomainA11yNode._internal(String label, Rectangle<int> boundingBox,
|
|
||||||
{@required this.location,
|
|
||||||
@required this.isRtl,
|
|
||||||
@required this.renderVertically,
|
|
||||||
OnFocus onFocus})
|
|
||||||
: super(label, boundingBox, onFocus: onFocus);
|
|
||||||
|
|
||||||
@override
|
|
||||||
int compareTo(_DomainA11yNode other) {
|
|
||||||
// Ordered by smaller location first, unless rendering vertically and RTL,
|
|
||||||
// then flip to sort by larger location first.
|
|
||||||
int result = location.compareTo(other.location);
|
|
||||||
|
|
||||||
if (renderVertically && isRtl && result != 0) {
|
|
||||||
result = -result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../data/series.dart' show AttributeKey;
|
|
||||||
import '../../base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
import '../../behavior/chart_behavior.dart' show ChartBehavior;
|
|
||||||
import '../../processed_series.dart' show MutableSeries;
|
|
||||||
|
|
||||||
const percentInjectedKey =
|
|
||||||
AttributeKey<bool>('PercentInjector.percentInjected');
|
|
||||||
|
|
||||||
/// Chart behavior that can inject series or domain percentages into each datum.
|
|
||||||
///
|
|
||||||
/// [totalType] configures the type of total to be calculated.
|
|
||||||
///
|
|
||||||
/// The measure values of each datum will be replaced by the percent of the
|
|
||||||
/// total measure value that each represents. The "raw" measure accessor
|
|
||||||
/// function on [MutableSeries] can still be used to get the original values.
|
|
||||||
///
|
|
||||||
/// Note that the results for measureLowerBound and measureUpperBound are not
|
|
||||||
/// currently well defined when converted into percentage values. This behavior
|
|
||||||
/// will replace them as percents to prevent bad axis results, but no effort is
|
|
||||||
/// made to bound them to within a "0 to 100%" data range.
|
|
||||||
///
|
|
||||||
/// Note that if the chart has a [Legend] that is capable of hiding series data,
|
|
||||||
/// then this behavior must be added after the [Legend] to ensure that it
|
|
||||||
/// calculates values after series have been potentially removed from the list.
|
|
||||||
class PercentInjector<D> implements ChartBehavior<D> {
|
|
||||||
LifecycleListener<D> _lifecycleListener;
|
|
||||||
|
|
||||||
/// The type of data total to be calculated.
|
|
||||||
final PercentInjectorTotalType totalType;
|
|
||||||
|
|
||||||
/// Constructs a [PercentInjector].
|
|
||||||
///
|
|
||||||
/// [totalType] configures the type of data total to be calculated.
|
|
||||||
PercentInjector({this.totalType = PercentInjectorTotalType.domain}) {
|
|
||||||
// Set up chart draw cycle listeners.
|
|
||||||
_lifecycleListener =
|
|
||||||
LifecycleListener<D>(onPreprocess: _preProcess, onData: _onData);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
chart.addLifecycleListener(_lifecycleListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart<D> chart) {
|
|
||||||
chart.removeLifecycleListener(_lifecycleListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the state of the behavior when new data is drawn on the chart.
|
|
||||||
void _onData(List<MutableSeries<D>> seriesList) {
|
|
||||||
// Reset tracking of percentage injection for new data.
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
series.setAttr(percentInjectedKey, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Injects percent of domain and/or series accessor functions into each
|
|
||||||
/// series.
|
|
||||||
///
|
|
||||||
/// These are injected in the preProcess phase in case other behaviors modify
|
|
||||||
/// the [seriesList] between chart redraws.
|
|
||||||
void _preProcess(List<MutableSeries<D>> seriesList) {
|
|
||||||
var percentInjected = true;
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
percentInjected = percentInjected && series.getAttr(percentInjectedKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (percentInjected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (totalType) {
|
|
||||||
case PercentInjectorTotalType.domain:
|
|
||||||
case PercentInjectorTotalType.domainBySeriesCategory:
|
|
||||||
final totalsByDomain = <String, num>{};
|
|
||||||
|
|
||||||
final useSeriesCategory =
|
|
||||||
totalType == PercentInjectorTotalType.domainBySeriesCategory;
|
|
||||||
|
|
||||||
// Walk the series and compute the domain total. Series total is
|
|
||||||
// automatically computed by [MutableSeries].
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
final seriesCategory = series.seriesCategory;
|
|
||||||
final rawMeasureFn = series.rawMeasureFn;
|
|
||||||
final domainFn = series.domainFn;
|
|
||||||
|
|
||||||
for (var index = 0; index < series.data.length; index++) {
|
|
||||||
final domain = domainFn(index);
|
|
||||||
var measure = rawMeasureFn(index);
|
|
||||||
measure ??= 0.0;
|
|
||||||
|
|
||||||
final key = useSeriesCategory
|
|
||||||
? '${seriesCategory}__${domain.toString()}'
|
|
||||||
: '${domain.toString()}';
|
|
||||||
|
|
||||||
if (totalsByDomain[key] != null) {
|
|
||||||
totalsByDomain[key] = totalsByDomain[key] + measure;
|
|
||||||
} else {
|
|
||||||
totalsByDomain[key] = measure;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add percent of domain and series accessor functions.
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
// Replace the default measure accessor with one that computes the
|
|
||||||
// percentage.
|
|
||||||
series.measureFn = (index) {
|
|
||||||
final measure = series.rawMeasureFn(index);
|
|
||||||
|
|
||||||
if (measure == null || measure == 0.0) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
final domain = series.domainFn(index);
|
|
||||||
|
|
||||||
final key = useSeriesCategory
|
|
||||||
? '${series.seriesCategory}__${domain.toString()}'
|
|
||||||
: '${domain.toString()}';
|
|
||||||
|
|
||||||
return measure / totalsByDomain[key];
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replace the default measure lower bound accessor with one that
|
|
||||||
// computes the percentage.
|
|
||||||
if (series.measureLowerBoundFn != null) {
|
|
||||||
series.measureLowerBoundFn = (index) {
|
|
||||||
final measureLowerBound = series.rawMeasureLowerBoundFn(index);
|
|
||||||
|
|
||||||
if (measureLowerBound == null || measureLowerBound == 0.0) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
final domain = series.domainFn(index);
|
|
||||||
|
|
||||||
final key = useSeriesCategory
|
|
||||||
? '${series.seriesCategory}__${domain.toString()}'
|
|
||||||
: '${domain.toString()}';
|
|
||||||
|
|
||||||
return measureLowerBound / totalsByDomain[key];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the default measure upper bound accessor with one that
|
|
||||||
// computes the percentage.
|
|
||||||
if (series.measureUpperBoundFn != null) {
|
|
||||||
series.measureUpperBoundFn = (index) {
|
|
||||||
final measureUpperBound = series.rawMeasureUpperBoundFn(index);
|
|
||||||
|
|
||||||
if (measureUpperBound == null || measureUpperBound == 0.0) {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
final domain = series.domainFn(index);
|
|
||||||
|
|
||||||
final key = useSeriesCategory
|
|
||||||
? '${series.seriesCategory}__${domain.toString()}'
|
|
||||||
: '${domain.toString()}';
|
|
||||||
|
|
||||||
return measureUpperBound / totalsByDomain[key];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
series.setAttr(percentInjectedKey, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PercentInjectorTotalType.series:
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
// Replace the default measure accessor with one that computes the
|
|
||||||
// percentage.
|
|
||||||
series.measureFn =
|
|
||||||
(index) => series.rawMeasureFn(index) / series.seriesMeasureTotal;
|
|
||||||
|
|
||||||
// Replace the default measure lower bound accessor with one that
|
|
||||||
// computes the percentage.
|
|
||||||
if (series.measureLowerBoundFn != null) {
|
|
||||||
series.measureLowerBoundFn = (index) =>
|
|
||||||
series.rawMeasureLowerBoundFn(index) /
|
|
||||||
series.seriesMeasureTotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the default measure upper bound accessor with one that
|
|
||||||
// computes the percentage.
|
|
||||||
if (series.measureUpperBoundFn != null) {
|
|
||||||
series.measureUpperBoundFn = (index) =>
|
|
||||||
series.rawMeasureUpperBoundFn(index) /
|
|
||||||
series.seriesMeasureTotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
series.setAttr(percentInjectedKey, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw ArgumentError('Unsupported totalType: $totalType');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'PercentInjector';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes the type of data total that will be calculated by PercentInjector.
|
|
||||||
///
|
|
||||||
/// [domain] calculates the percentage of each datum's measure value out of the
|
|
||||||
/// total measure values for all data that share the same domain value.
|
|
||||||
///
|
|
||||||
/// [domainBySeriesCategory] calculates the percentage of each datum's measure
|
|
||||||
/// value out of the total measure values for all data that share the same
|
|
||||||
/// domain value and seriesCategory value. This should be enabled if the data
|
|
||||||
/// will be rendered by a series renderer that groups data by both domain and
|
|
||||||
/// series category, such as the "grouped stacked" mode of [BarRenderer].
|
|
||||||
///
|
|
||||||
/// [series] calculates the percentage of each datum's measure value out of the
|
|
||||||
/// total measure values for all data in that datum's series.
|
|
||||||
enum PercentInjectorTotalType { domain, domainBySeriesCategory, series }
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../base_chart.dart';
|
|
||||||
|
|
||||||
/// Interface for adding behavior to a chart.
|
|
||||||
///
|
|
||||||
/// For example pan and zoom are implemented via behavior strategies.
|
|
||||||
abstract class ChartBehavior<D> {
|
|
||||||
String get role;
|
|
||||||
|
|
||||||
/// Injects the behavior into a chart.
|
|
||||||
void attachTo(BaseChart<D> chart);
|
|
||||||
|
|
||||||
/// Removes the behavior from a chart.
|
|
||||||
void removeFrom(BaseChart<D> chart);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Position of a component within the chart layout.
|
|
||||||
///
|
|
||||||
/// Outside positions are [top], [bottom], [start], and [end].
|
|
||||||
///
|
|
||||||
/// [top] component positioned at the top, with the chart positioned below the
|
|
||||||
/// component and height reduced by the height of the component.
|
|
||||||
/// [bottom] component positioned below the chart, and the chart's height is
|
|
||||||
/// reduced by the height of the component.
|
|
||||||
/// [start] component is positioned at the left of the chart (or the right if
|
|
||||||
/// RTL), the chart's width is reduced by the width of the component.
|
|
||||||
/// [end] component is positioned at the right of the chart (or the left if
|
|
||||||
/// RTL), the chart's width is reduced by the width of the component.
|
|
||||||
/// [inside] component is layered on top of the chart.
|
|
||||||
enum BehaviorPosition {
|
|
||||||
top,
|
|
||||||
bottom,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
inside,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Justification for components positioned outside [BehaviorPosition].
|
|
||||||
enum OutsideJustification {
|
|
||||||
startDrawArea,
|
|
||||||
start,
|
|
||||||
middleDrawArea,
|
|
||||||
middle,
|
|
||||||
endDrawArea,
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Justification for components positioned [BehaviorPosition.inside].
|
|
||||||
enum InsideJustification {
|
|
||||||
topStart,
|
|
||||||
topEnd,
|
|
||||||
}
|
|
||||||
@@ -1,828 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
|
||||||
import '../../../../common/text_element.dart'
|
|
||||||
show MaxWidthStrategy, TextDirection, TextElement;
|
|
||||||
import '../../../../common/text_style.dart' show TextStyle;
|
|
||||||
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
|
|
||||||
import '../../../layout/layout_view.dart'
|
|
||||||
show
|
|
||||||
LayoutPosition,
|
|
||||||
LayoutView,
|
|
||||||
LayoutViewConfig,
|
|
||||||
LayoutViewPaintOrder,
|
|
||||||
LayoutViewPositionOrder,
|
|
||||||
ViewMeasuredSizes;
|
|
||||||
import '../../base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
import '../../behavior/chart_behavior.dart'
|
|
||||||
show BehaviorPosition, ChartBehavior, OutsideJustification;
|
|
||||||
import '../../chart_canvas.dart' show ChartCanvas;
|
|
||||||
|
|
||||||
/// Chart behavior that adds title text to a chart. An optional second line of
|
|
||||||
/// text may be rendered as a sub-title.
|
|
||||||
///
|
|
||||||
/// Titles will by default be rendered as the outermost component in the chart
|
|
||||||
/// margin.
|
|
||||||
class ChartTitle<D> implements ChartBehavior<D> {
|
|
||||||
static const _defaultBehaviorPosition = BehaviorPosition.top;
|
|
||||||
static const _defaultMaxWidthStrategy = MaxWidthStrategy.ellipsize;
|
|
||||||
static const _defaultTitleDirection = ChartTitleDirection.auto;
|
|
||||||
static const _defaultTitleOutsideJustification = OutsideJustification.middle;
|
|
||||||
static final _defaultTitleStyle =
|
|
||||||
TextStyleSpec(fontSize: 18, color: StyleFactory.style.tickColor);
|
|
||||||
static final _defaultSubTitleStyle =
|
|
||||||
TextStyleSpec(fontSize: 14, color: StyleFactory.style.tickColor);
|
|
||||||
static const _defaultInnerPadding = 10;
|
|
||||||
static const _defaultTitlePadding = 18;
|
|
||||||
static const _defaultOuterPadding = 10;
|
|
||||||
|
|
||||||
/// Stores all of the configured properties of the behavior.
|
|
||||||
_ChartTitleConfig _config;
|
|
||||||
|
|
||||||
BaseChart<D> _chart;
|
|
||||||
|
|
||||||
_ChartTitleLayoutView _view;
|
|
||||||
|
|
||||||
LifecycleListener<D> _lifecycleListener;
|
|
||||||
|
|
||||||
/// Constructs a [ChartTitle].
|
|
||||||
///
|
|
||||||
/// [title] contains the text for the chart title.
|
|
||||||
ChartTitle(String title,
|
|
||||||
{BehaviorPosition behaviorPosition,
|
|
||||||
int innerPadding,
|
|
||||||
int layoutMinSize,
|
|
||||||
int layoutPreferredSize,
|
|
||||||
int outerPadding,
|
|
||||||
MaxWidthStrategy maxWidthStrategy,
|
|
||||||
ChartTitleDirection titleDirection,
|
|
||||||
OutsideJustification titleOutsideJustification,
|
|
||||||
int titlePadding,
|
|
||||||
TextStyleSpec titleStyleSpec,
|
|
||||||
String subTitle,
|
|
||||||
TextStyleSpec subTitleStyleSpec}) {
|
|
||||||
_config = _ChartTitleConfig()
|
|
||||||
..behaviorPosition = behaviorPosition ?? _defaultBehaviorPosition
|
|
||||||
..innerPadding = innerPadding ?? _defaultInnerPadding
|
|
||||||
..layoutMinSize = layoutMinSize
|
|
||||||
..layoutPreferredSize = layoutPreferredSize
|
|
||||||
..outerPadding = outerPadding ?? _defaultOuterPadding
|
|
||||||
..maxWidthStrategy = maxWidthStrategy ?? _defaultMaxWidthStrategy
|
|
||||||
..title = title
|
|
||||||
..titleDirection = titleDirection ?? _defaultTitleDirection
|
|
||||||
..titleOutsideJustification =
|
|
||||||
titleOutsideJustification ?? _defaultTitleOutsideJustification
|
|
||||||
..titlePadding = titlePadding ?? _defaultTitlePadding
|
|
||||||
..titleStyleSpec = titleStyleSpec ?? _defaultTitleStyle
|
|
||||||
..subTitle = subTitle
|
|
||||||
..subTitleStyleSpec = subTitleStyleSpec ?? _defaultSubTitleStyle;
|
|
||||||
|
|
||||||
_lifecycleListener =
|
|
||||||
LifecycleListener<D>(onAxisConfigured: _updateViewData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout position for the title.
|
|
||||||
BehaviorPosition get behaviorPosition => _config.behaviorPosition;
|
|
||||||
|
|
||||||
set behaviorPosition(BehaviorPosition behaviorPosition) {
|
|
||||||
_config.behaviorPosition = behaviorPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Minimum size of the legend component. Optional.
|
|
||||||
///
|
|
||||||
/// If the legend is positioned in the top or bottom margin, then this
|
|
||||||
/// configures the legend's height. If positioned in the start or end
|
|
||||||
/// position, this configures the legend's width.
|
|
||||||
int get layoutMinSize => _config.layoutMinSize;
|
|
||||||
|
|
||||||
set layoutMinSize(int layoutMinSize) {
|
|
||||||
_config.layoutMinSize = layoutMinSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Preferred size of the legend component. Defaults to 0.
|
|
||||||
///
|
|
||||||
/// If the legend is positioned in the top or bottom margin, then this
|
|
||||||
/// configures the legend's height. If positioned in the start or end
|
|
||||||
/// position, this configures the legend's width.
|
|
||||||
int get layoutPreferredSize => _config.layoutPreferredSize;
|
|
||||||
|
|
||||||
set layoutPreferredSize(int layoutPreferredSize) {
|
|
||||||
_config.layoutPreferredSize = layoutPreferredSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Strategy for handling title text that is too large to fit. Defaults to
|
|
||||||
/// truncating the text with ellipses.
|
|
||||||
MaxWidthStrategy get maxWidthStrategy => _config.maxWidthStrategy;
|
|
||||||
|
|
||||||
set maxWidthStrategy(MaxWidthStrategy maxWidthStrategy) {
|
|
||||||
_config.maxWidthStrategy = maxWidthStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Primary text for the title.
|
|
||||||
String get title => _config.title;
|
|
||||||
|
|
||||||
set title(String title) {
|
|
||||||
_config.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Direction of the chart title text.
|
|
||||||
///
|
|
||||||
/// This defaults to horizontal for a title in the top or bottom
|
|
||||||
/// [behaviorPosition], or vertical for start or end [behaviorPosition].
|
|
||||||
ChartTitleDirection get titleDirection => _config.titleDirection;
|
|
||||||
|
|
||||||
set titleDirection(ChartTitleDirection titleDirection) {
|
|
||||||
_config.titleDirection = titleDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Justification of the title text if it is positioned outside of the draw
|
|
||||||
/// area.
|
|
||||||
OutsideJustification get titleOutsideJustification =>
|
|
||||||
_config.titleOutsideJustification;
|
|
||||||
|
|
||||||
set titleOutsideJustification(
|
|
||||||
OutsideJustification titleOutsideJustification) {
|
|
||||||
_config.titleOutsideJustification = titleOutsideJustification;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Space between the title and sub-title text, if defined.
|
|
||||||
///
|
|
||||||
/// This padding is not used if no sub-title is provided.
|
|
||||||
int get titlePadding => _config.titlePadding;
|
|
||||||
|
|
||||||
set titlePadding(int titlePadding) {
|
|
||||||
_config.titlePadding = titlePadding;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Style of the [title] text.
|
|
||||||
TextStyleSpec get titleStyleSpec => _config.titleStyleSpec;
|
|
||||||
|
|
||||||
set titleStyleSpec(TextStyleSpec titleStyleSpec) {
|
|
||||||
_config.titleStyleSpec = titleStyleSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Secondary text for the sub-title.
|
|
||||||
///
|
|
||||||
/// [subTitle] is rendered on a second line below the [title], and may be
|
|
||||||
/// styled differently.
|
|
||||||
String get subTitle => _config.subTitle;
|
|
||||||
|
|
||||||
set subTitle(String subTitle) {
|
|
||||||
_config.subTitle = subTitle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Style of the [subTitle] text.
|
|
||||||
TextStyleSpec get subTitleStyleSpec => _config.subTitleStyleSpec;
|
|
||||||
|
|
||||||
set subTitleStyleSpec(TextStyleSpec subTitleStyleSpec) {
|
|
||||||
_config.subTitleStyleSpec = subTitleStyleSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Space between the "inside" of the chart, and the title behavior itself.
|
|
||||||
///
|
|
||||||
/// This padding is applied to all the edge of the title that is in the
|
|
||||||
/// direction of the draw area. For a top positioned title, this is applied
|
|
||||||
/// to the bottom edge. [outerPadding] is applied to the top, left, and right
|
|
||||||
/// edges.
|
|
||||||
///
|
|
||||||
/// If a sub-title is defined, this is the space between the sub-title text
|
|
||||||
/// and the inside of the chart. Otherwise, it is the space between the title
|
|
||||||
/// text and the inside of chart.
|
|
||||||
int get innerPadding => _config.innerPadding;
|
|
||||||
|
|
||||||
set innerPadding(int innerPadding) {
|
|
||||||
_config.innerPadding = innerPadding;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Space between the "outside" of the chart, and the title behavior itself.
|
|
||||||
///
|
|
||||||
/// This padding is applied to all 3 edges of the title that are not in the
|
|
||||||
/// direction of the draw area. For a top positioned title, this is applied
|
|
||||||
/// to the top, left, and right edges. [innerPadding] is applied to the
|
|
||||||
/// bottom edge.
|
|
||||||
int get outerPadding => _config.outerPadding;
|
|
||||||
|
|
||||||
set outerPadding(int outerPadding) {
|
|
||||||
_config.outerPadding = outerPadding;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
_chart = chart;
|
|
||||||
|
|
||||||
_view = _ChartTitleLayoutView<D>(
|
|
||||||
layoutPaintOrder: LayoutViewPaintOrder.chartTitle,
|
|
||||||
config: _config,
|
|
||||||
chart: _chart);
|
|
||||||
|
|
||||||
chart.addView(_view);
|
|
||||||
chart.addLifecycleListener(_lifecycleListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart<D> chart) {
|
|
||||||
chart.removeView(_view);
|
|
||||||
chart.removeLifecycleListener(_lifecycleListener);
|
|
||||||
_chart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateViewData() {
|
|
||||||
_view.config = _config;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'ChartTitle-${_config?.behaviorPosition}';
|
|
||||||
|
|
||||||
bool get isRtl => _chart.context.isRtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Layout view component for [ChartTitle].
|
|
||||||
class _ChartTitleLayoutView<D> extends LayoutView {
|
|
||||||
LayoutViewConfig _layoutConfig;
|
|
||||||
|
|
||||||
LayoutViewConfig get layoutConfig => _layoutConfig;
|
|
||||||
|
|
||||||
/// Stores all of the configured properties of the behavior.
|
|
||||||
_ChartTitleConfig _config;
|
|
||||||
|
|
||||||
BaseChart<D> chart;
|
|
||||||
|
|
||||||
bool get isRtl => chart?.context?.isRtl ?? false;
|
|
||||||
|
|
||||||
Rectangle<int> _componentBounds;
|
|
||||||
Rectangle<int> _drawAreaBounds;
|
|
||||||
|
|
||||||
GraphicsFactory graphicsFactory;
|
|
||||||
|
|
||||||
/// Cached layout element for the title text.
|
|
||||||
///
|
|
||||||
/// This is used to prevent expensive Flutter painter layout calls on every
|
|
||||||
/// animation frame during the paint cycle. It should never be cached during
|
|
||||||
/// layout measurement.
|
|
||||||
TextElement _titleTextElement;
|
|
||||||
|
|
||||||
/// Cached layout element for the sub-title text.
|
|
||||||
///
|
|
||||||
/// This is used to prevent expensive Flutter painter layout calls on every
|
|
||||||
/// animation frame during the paint cycle. It should never be cached during
|
|
||||||
/// layout measurement.
|
|
||||||
TextElement _subTitleTextElement;
|
|
||||||
|
|
||||||
_ChartTitleLayoutView(
|
|
||||||
{@required int layoutPaintOrder,
|
|
||||||
@required _ChartTitleConfig config,
|
|
||||||
@required this.chart})
|
|
||||||
: this._config = config {
|
|
||||||
// Set inside body to resolve [_layoutPosition].
|
|
||||||
_layoutConfig = LayoutViewConfig(
|
|
||||||
paintOrder: layoutPaintOrder,
|
|
||||||
position: _layoutPosition,
|
|
||||||
positionOrder: LayoutViewPositionOrder.chartTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the configuration for the title behavior.
|
|
||||||
set config(_ChartTitleConfig config) {
|
|
||||||
_config = config;
|
|
||||||
layoutConfig.position = _layoutPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
|
|
||||||
int minWidth;
|
|
||||||
int minHeight;
|
|
||||||
int preferredWidth = 0;
|
|
||||||
int preferredHeight = 0;
|
|
||||||
|
|
||||||
// Always assume that we need outer padding and title padding, but only add
|
|
||||||
// in the sub-title padding if we have one. Title is required, but sub-title
|
|
||||||
// is optional.
|
|
||||||
final totalPadding = _config.outerPadding +
|
|
||||||
_config.innerPadding +
|
|
||||||
(_config.subTitle != null ? _config.titlePadding : 0.0);
|
|
||||||
|
|
||||||
// Create [TextStyle] from [TextStyleSpec] to be used by all the elements.
|
|
||||||
// The [GraphicsFactory] is needed so it can't be created earlier.
|
|
||||||
final textStyle = _getTextStyle(graphicsFactory, _config.titleStyleSpec);
|
|
||||||
|
|
||||||
final textElement = graphicsFactory.createTextElement(_config.title)
|
|
||||||
..maxWidthStrategy = _config.maxWidthStrategy
|
|
||||||
..textStyle = textStyle;
|
|
||||||
|
|
||||||
final subTitleTextStyle =
|
|
||||||
_getTextStyle(graphicsFactory, _config.subTitleStyleSpec);
|
|
||||||
|
|
||||||
final subTitleTextElement =
|
|
||||||
graphicsFactory.createTextElement(_config.subTitle)
|
|
||||||
..maxWidthStrategy = _config.maxWidthStrategy
|
|
||||||
..textStyle = subTitleTextStyle;
|
|
||||||
|
|
||||||
final resolvedTitleDirection = _resolvedTitleDirection;
|
|
||||||
|
|
||||||
switch (_config.behaviorPosition) {
|
|
||||||
case BehaviorPosition.bottom:
|
|
||||||
case BehaviorPosition.top:
|
|
||||||
final textHeight =
|
|
||||||
(resolvedTitleDirection == ChartTitleDirection.vertical
|
|
||||||
? textElement.measurement.horizontalSliceWidth
|
|
||||||
: textElement.measurement.verticalSliceWidth)
|
|
||||||
.round();
|
|
||||||
|
|
||||||
final subTitleTextHeight = _config.subTitle != null
|
|
||||||
? (resolvedTitleDirection == ChartTitleDirection.vertical
|
|
||||||
? subTitleTextElement.measurement.horizontalSliceWidth
|
|
||||||
: subTitleTextElement.measurement.verticalSliceWidth)
|
|
||||||
.round()
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
final measuredHeight =
|
|
||||||
(textHeight + subTitleTextHeight + totalPadding).round();
|
|
||||||
minHeight = _config.layoutMinSize != null
|
|
||||||
? min(_config.layoutMinSize, measuredHeight)
|
|
||||||
: measuredHeight;
|
|
||||||
|
|
||||||
preferredWidth = maxWidth;
|
|
||||||
|
|
||||||
preferredHeight = _config.layoutPreferredSize != null
|
|
||||||
? min(_config.layoutPreferredSize, maxHeight)
|
|
||||||
: measuredHeight;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BehaviorPosition.end:
|
|
||||||
case BehaviorPosition.start:
|
|
||||||
final textWidth =
|
|
||||||
(resolvedTitleDirection == ChartTitleDirection.vertical
|
|
||||||
? textElement.measurement.verticalSliceWidth
|
|
||||||
: textElement.measurement.horizontalSliceWidth)
|
|
||||||
.round();
|
|
||||||
|
|
||||||
final subTitleTextWidth = _config.subTitle != null
|
|
||||||
? (resolvedTitleDirection == ChartTitleDirection.vertical
|
|
||||||
? subTitleTextElement.measurement.verticalSliceWidth
|
|
||||||
: subTitleTextElement.measurement.horizontalSliceWidth)
|
|
||||||
.round()
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
final measuredWidth =
|
|
||||||
(textWidth + subTitleTextWidth + totalPadding).round();
|
|
||||||
minWidth = _config.layoutMinSize != null
|
|
||||||
? min(_config.layoutMinSize, measuredWidth)
|
|
||||||
: measuredWidth;
|
|
||||||
|
|
||||||
preferredWidth = _config.layoutPreferredSize != null
|
|
||||||
? min(_config.layoutPreferredSize, maxWidth)
|
|
||||||
: measuredWidth;
|
|
||||||
|
|
||||||
preferredHeight = maxHeight;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BehaviorPosition.inside:
|
|
||||||
preferredWidth = _drawAreaBounds != null
|
|
||||||
? min(_drawAreaBounds.width, maxWidth)
|
|
||||||
: maxWidth;
|
|
||||||
|
|
||||||
preferredHeight = _drawAreaBounds != null
|
|
||||||
? min(_drawAreaBounds.height, maxHeight)
|
|
||||||
: maxHeight;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the cached text elements used during the paint step.
|
|
||||||
_resetTextElementCache();
|
|
||||||
|
|
||||||
return ViewMeasuredSizes(
|
|
||||||
minWidth: minWidth,
|
|
||||||
minHeight: minHeight,
|
|
||||||
preferredWidth: preferredWidth,
|
|
||||||
preferredHeight: preferredHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
|
|
||||||
this._componentBounds = componentBounds;
|
|
||||||
this._drawAreaBounds = drawAreaBounds;
|
|
||||||
|
|
||||||
// Reset the cached text elements used during the paint step.
|
|
||||||
_resetTextElementCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(ChartCanvas canvas, double animationPercent) {
|
|
||||||
final resolvedTitleDirection = _resolvedTitleDirection;
|
|
||||||
|
|
||||||
var titleHeight = 0.0;
|
|
||||||
var subTitleHeight = 0.0;
|
|
||||||
|
|
||||||
// First, measure the height of the title and sub-title.
|
|
||||||
if (_config.title != null) {
|
|
||||||
// Chart titles do not animate. As an optimization for Flutter, cache the
|
|
||||||
// [TextElement] to avoid an expensive painter layout operation on
|
|
||||||
// subsequent animation frames.
|
|
||||||
if (_titleTextElement == null) {
|
|
||||||
// Create [TextStyle] from [TextStyleSpec] to be used by all the
|
|
||||||
// elements. The [GraphicsFactory] is needed so it can't be created
|
|
||||||
// earlier.
|
|
||||||
final textStyle =
|
|
||||||
_getTextStyle(graphicsFactory, _config.titleStyleSpec);
|
|
||||||
|
|
||||||
_titleTextElement = graphicsFactory.createTextElement(_config.title)
|
|
||||||
..maxWidthStrategy = _config.maxWidthStrategy
|
|
||||||
..textStyle = textStyle;
|
|
||||||
|
|
||||||
_titleTextElement.maxWidth =
|
|
||||||
resolvedTitleDirection == ChartTitleDirection.horizontal
|
|
||||||
? _componentBounds.width
|
|
||||||
: _componentBounds.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the height of the title so that we can off-set both text elements.
|
|
||||||
titleHeight = _titleTextElement.measurement.verticalSliceWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_config.subTitle != null) {
|
|
||||||
// Chart titles do not animate. As an optimization for Flutter, cache the
|
|
||||||
// [TextElement] to avoid an expensive painter layout operation on
|
|
||||||
// subsequent animation frames.
|
|
||||||
if (_subTitleTextElement == null) {
|
|
||||||
// Create [TextStyle] from [TextStyleSpec] to be used by all the
|
|
||||||
// elements. The [GraphicsFactory] is needed so it can't be created
|
|
||||||
// earlier.
|
|
||||||
final textStyle =
|
|
||||||
_getTextStyle(graphicsFactory, _config.subTitleStyleSpec);
|
|
||||||
|
|
||||||
_subTitleTextElement =
|
|
||||||
graphicsFactory.createTextElement(_config.subTitle)
|
|
||||||
..maxWidthStrategy = _config.maxWidthStrategy
|
|
||||||
..textStyle = textStyle;
|
|
||||||
|
|
||||||
_subTitleTextElement.maxWidth =
|
|
||||||
resolvedTitleDirection == ChartTitleDirection.horizontal
|
|
||||||
? _componentBounds.width
|
|
||||||
: _componentBounds.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the height of the sub-title so that we can off-set both text
|
|
||||||
// elements.
|
|
||||||
subTitleHeight = _subTitleTextElement.measurement.verticalSliceWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw a title if the text is not empty.
|
|
||||||
if (_config.title != null) {
|
|
||||||
final labelPoint = _getLabelPosition(
|
|
||||||
true,
|
|
||||||
_componentBounds,
|
|
||||||
resolvedTitleDirection,
|
|
||||||
_titleTextElement,
|
|
||||||
titleHeight,
|
|
||||||
subTitleHeight);
|
|
||||||
|
|
||||||
if (labelPoint != null) {
|
|
||||||
final rotation = resolvedTitleDirection == ChartTitleDirection.vertical
|
|
||||||
? -pi / 2
|
|
||||||
: 0.0;
|
|
||||||
|
|
||||||
canvas.drawText(_titleTextElement, labelPoint.x, labelPoint.y,
|
|
||||||
rotation: rotation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw a sub-title if the text is not empty.
|
|
||||||
if (_config.subTitle != null) {
|
|
||||||
final labelPoint = _getLabelPosition(
|
|
||||||
false,
|
|
||||||
_componentBounds,
|
|
||||||
resolvedTitleDirection,
|
|
||||||
_subTitleTextElement,
|
|
||||||
titleHeight,
|
|
||||||
subTitleHeight);
|
|
||||||
|
|
||||||
if (labelPoint != null) {
|
|
||||||
final rotation = resolvedTitleDirection == ChartTitleDirection.vertical
|
|
||||||
? -pi / 2
|
|
||||||
: 0.0;
|
|
||||||
|
|
||||||
canvas.drawText(_subTitleTextElement, labelPoint.x, labelPoint.y,
|
|
||||||
rotation: rotation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the cached text elements used during the paint step.
|
|
||||||
void _resetTextElementCache() {
|
|
||||||
_titleTextElement = null;
|
|
||||||
_subTitleTextElement = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the direction of the title, resolving "auto" position into the
|
|
||||||
/// appropriate direction for the position of the behavior.
|
|
||||||
ChartTitleDirection get _resolvedTitleDirection {
|
|
||||||
var resolvedTitleDirection = _config.titleDirection;
|
|
||||||
if (resolvedTitleDirection == ChartTitleDirection.auto) {
|
|
||||||
switch (_config.behaviorPosition) {
|
|
||||||
case BehaviorPosition.bottom:
|
|
||||||
case BehaviorPosition.inside:
|
|
||||||
case BehaviorPosition.top:
|
|
||||||
resolvedTitleDirection = ChartTitleDirection.horizontal;
|
|
||||||
break;
|
|
||||||
case BehaviorPosition.end:
|
|
||||||
case BehaviorPosition.start:
|
|
||||||
resolvedTitleDirection = ChartTitleDirection.vertical;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedTitleDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get layout position from chart title position.
|
|
||||||
LayoutPosition get _layoutPosition {
|
|
||||||
LayoutPosition position;
|
|
||||||
switch (_config.behaviorPosition) {
|
|
||||||
case BehaviorPosition.bottom:
|
|
||||||
position = LayoutPosition.Bottom;
|
|
||||||
break;
|
|
||||||
case BehaviorPosition.end:
|
|
||||||
position = isRtl ? LayoutPosition.Left : LayoutPosition.Right;
|
|
||||||
break;
|
|
||||||
case BehaviorPosition.inside:
|
|
||||||
position = LayoutPosition.DrawArea;
|
|
||||||
break;
|
|
||||||
case BehaviorPosition.start:
|
|
||||||
position = isRtl ? LayoutPosition.Right : LayoutPosition.Left;
|
|
||||||
break;
|
|
||||||
case BehaviorPosition.top:
|
|
||||||
position = LayoutPosition.Top;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a "full" [OutsideJustification], convert the layout position
|
|
||||||
// to the "full" form.
|
|
||||||
if (_config.titleOutsideJustification == OutsideJustification.start ||
|
|
||||||
_config.titleOutsideJustification == OutsideJustification.middle ||
|
|
||||||
_config.titleOutsideJustification == OutsideJustification.end) {
|
|
||||||
switch (position) {
|
|
||||||
case LayoutPosition.Bottom:
|
|
||||||
position = LayoutPosition.FullBottom;
|
|
||||||
break;
|
|
||||||
case LayoutPosition.Left:
|
|
||||||
position = LayoutPosition.FullLeft;
|
|
||||||
break;
|
|
||||||
case LayoutPosition.Top:
|
|
||||||
position = LayoutPosition.FullTop;
|
|
||||||
break;
|
|
||||||
case LayoutPosition.Right:
|
|
||||||
position = LayoutPosition.FullRight;
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Ignore other positions, like DrawArea.
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the resolved location for a label element.
|
|
||||||
Point<int> _getLabelPosition(
|
|
||||||
bool isPrimaryTitle,
|
|
||||||
Rectangle<num> bounds,
|
|
||||||
ChartTitleDirection titleDirection,
|
|
||||||
TextElement textElement,
|
|
||||||
double titleHeight,
|
|
||||||
double subTitleHeight) {
|
|
||||||
switch (_config.behaviorPosition) {
|
|
||||||
case BehaviorPosition.bottom:
|
|
||||||
case BehaviorPosition.top:
|
|
||||||
return _getHorizontalLabelPosition(isPrimaryTitle, bounds,
|
|
||||||
titleDirection, textElement, titleHeight, subTitleHeight);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BehaviorPosition.start:
|
|
||||||
case BehaviorPosition.end:
|
|
||||||
return _getVerticalLabelPosition(isPrimaryTitle, bounds, titleDirection,
|
|
||||||
textElement, titleHeight, subTitleHeight);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BehaviorPosition.inside:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the resolved location for a title in the top or bottom margin.
|
|
||||||
Point<int> _getHorizontalLabelPosition(
|
|
||||||
bool isPrimaryTitle,
|
|
||||||
Rectangle<num> bounds,
|
|
||||||
ChartTitleDirection titleDirection,
|
|
||||||
TextElement textElement,
|
|
||||||
double titleHeight,
|
|
||||||
double subTitleHeight) {
|
|
||||||
int labelX = 0;
|
|
||||||
int labelY = 0;
|
|
||||||
|
|
||||||
switch (_config.titleOutsideJustification) {
|
|
||||||
case OutsideJustification.middle:
|
|
||||||
case OutsideJustification.middleDrawArea:
|
|
||||||
final textWidth =
|
|
||||||
(isRtl ? 1 : -1) * textElement.measurement.horizontalSliceWidth / 2;
|
|
||||||
labelX = (bounds.left + bounds.width / 2 + textWidth).round();
|
|
||||||
|
|
||||||
textElement.textDirection =
|
|
||||||
isRtl ? TextDirection.rtl : TextDirection.ltr;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case OutsideJustification.end:
|
|
||||||
case OutsideJustification.endDrawArea:
|
|
||||||
case OutsideJustification.start:
|
|
||||||
case OutsideJustification.startDrawArea:
|
|
||||||
final alignLeft = isRtl
|
|
||||||
? (_config.titleOutsideJustification == OutsideJustification.end ||
|
|
||||||
_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.endDrawArea)
|
|
||||||
: (_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.start ||
|
|
||||||
_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.startDrawArea);
|
|
||||||
|
|
||||||
// Don't apply outer padding if we are aligned to the draw area.
|
|
||||||
final padding = (_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.endDrawArea ||
|
|
||||||
_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.startDrawArea)
|
|
||||||
? 0.0
|
|
||||||
: _config.outerPadding;
|
|
||||||
|
|
||||||
if (alignLeft) {
|
|
||||||
labelX = (bounds.left + padding).round();
|
|
||||||
textElement.textDirection = TextDirection.ltr;
|
|
||||||
} else {
|
|
||||||
labelX = (bounds.right - padding).round();
|
|
||||||
textElement.textDirection = TextDirection.rtl;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// labelY is always relative to the component bounds.
|
|
||||||
if (_config.behaviorPosition == BehaviorPosition.bottom) {
|
|
||||||
final padding = _config.innerPadding +
|
|
||||||
(isPrimaryTitle ? 0 : _config.titlePadding + titleHeight);
|
|
||||||
|
|
||||||
labelY = (bounds.top + padding).round();
|
|
||||||
} else {
|
|
||||||
var padding = 0.0 + _config.innerPadding;
|
|
||||||
if (isPrimaryTitle) {
|
|
||||||
padding +=
|
|
||||||
((subTitleHeight > 0 ? _config.titlePadding + subTitleHeight : 0) +
|
|
||||||
titleHeight);
|
|
||||||
} else {
|
|
||||||
padding += subTitleHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
labelY = (bounds.bottom - padding).round();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Point<int>(labelX, labelY);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the resolved location for a title in the left or right margin.
|
|
||||||
Point<int> _getVerticalLabelPosition(
|
|
||||||
bool isPrimaryTitle,
|
|
||||||
Rectangle<num> bounds,
|
|
||||||
ChartTitleDirection titleDirection,
|
|
||||||
TextElement textElement,
|
|
||||||
double titleHeight,
|
|
||||||
double subTitleHeight) {
|
|
||||||
int labelX = 0;
|
|
||||||
int labelY = 0;
|
|
||||||
|
|
||||||
switch (_config.titleOutsideJustification) {
|
|
||||||
case OutsideJustification.middle:
|
|
||||||
case OutsideJustification.middleDrawArea:
|
|
||||||
final textWidth =
|
|
||||||
(isRtl ? -1 : 1) * textElement.measurement.horizontalSliceWidth / 2;
|
|
||||||
labelY = (bounds.top + bounds.height / 2 + textWidth).round();
|
|
||||||
|
|
||||||
textElement.textDirection =
|
|
||||||
isRtl ? TextDirection.rtl : TextDirection.ltr;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case OutsideJustification.end:
|
|
||||||
case OutsideJustification.endDrawArea:
|
|
||||||
case OutsideJustification.start:
|
|
||||||
case OutsideJustification.startDrawArea:
|
|
||||||
final alignLeft = isRtl
|
|
||||||
? (_config.titleOutsideJustification == OutsideJustification.end ||
|
|
||||||
_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.endDrawArea)
|
|
||||||
: (_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.start ||
|
|
||||||
_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.startDrawArea);
|
|
||||||
|
|
||||||
// Don't apply outer padding if we are aligned to the draw area.
|
|
||||||
final padding = (_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.endDrawArea ||
|
|
||||||
_config.titleOutsideJustification ==
|
|
||||||
OutsideJustification.startDrawArea)
|
|
||||||
? 0.0
|
|
||||||
: _config.outerPadding;
|
|
||||||
|
|
||||||
if (alignLeft) {
|
|
||||||
labelY = (bounds.bottom - padding).round();
|
|
||||||
textElement.textDirection = TextDirection.ltr;
|
|
||||||
} else {
|
|
||||||
labelY = (bounds.top + padding).round();
|
|
||||||
textElement.textDirection = TextDirection.rtl;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// labelX is always relative to the component bounds.
|
|
||||||
if (_layoutPosition == LayoutPosition.Right ||
|
|
||||||
_layoutPosition == LayoutPosition.FullRight) {
|
|
||||||
final padding = _config.outerPadding +
|
|
||||||
(isPrimaryTitle ? 0 : _config.titlePadding + titleHeight);
|
|
||||||
|
|
||||||
labelX = (bounds.left + padding).round();
|
|
||||||
} else {
|
|
||||||
final padding = _config.outerPadding +
|
|
||||||
titleHeight +
|
|
||||||
(isPrimaryTitle
|
|
||||||
? (subTitleHeight > 0 ? _config.titlePadding + subTitleHeight : 0)
|
|
||||||
: 0.0);
|
|
||||||
|
|
||||||
labelX = (bounds.right - padding).round();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Point<int>(labelX, labelY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function that converts [TextStyleSpec] to [TextStyle].
|
|
||||||
TextStyle _getTextStyle(
|
|
||||||
GraphicsFactory graphicsFactory, TextStyleSpec labelSpec) {
|
|
||||||
return graphicsFactory.createTextPaint()
|
|
||||||
..color = labelSpec?.color ?? StyleFactory.style.tickColor
|
|
||||||
..fontFamily = labelSpec?.fontFamily
|
|
||||||
..fontSize = labelSpec?.fontSize ?? 18;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Rectangle<int> get componentBounds => this._drawAreaBounds;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isSeriesRenderer => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration object for [ChartTitle].
|
|
||||||
class _ChartTitleConfig {
|
|
||||||
BehaviorPosition behaviorPosition;
|
|
||||||
|
|
||||||
int layoutMinSize;
|
|
||||||
int layoutPreferredSize;
|
|
||||||
|
|
||||||
MaxWidthStrategy maxWidthStrategy;
|
|
||||||
|
|
||||||
String title;
|
|
||||||
ChartTitleDirection titleDirection;
|
|
||||||
OutsideJustification titleOutsideJustification;
|
|
||||||
TextStyleSpec titleStyleSpec;
|
|
||||||
|
|
||||||
String subTitle;
|
|
||||||
TextStyleSpec subTitleStyleSpec;
|
|
||||||
|
|
||||||
int innerPadding;
|
|
||||||
int titlePadding;
|
|
||||||
int outerPadding;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Direction of the title text on the chart.
|
|
||||||
enum ChartTitleDirection {
|
|
||||||
/// Automatically assign a direction based on the [RangeAnnotationAxisType].
|
|
||||||
///
|
|
||||||
/// [horizontal] for measure axes, or [vertical] for domain axes.
|
|
||||||
auto,
|
|
||||||
|
|
||||||
/// Text flows parallel to the x axis.
|
|
||||||
horizontal,
|
|
||||||
|
|
||||||
/// Text flows parallel to the y axis.
|
|
||||||
vertical,
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
import '../processed_series.dart' show MutableSeries;
|
|
||||||
import '../selection_model/selection_model.dart'
|
|
||||||
show SelectionModel, SelectionModelType;
|
|
||||||
import 'chart_behavior.dart' show ChartBehavior;
|
|
||||||
|
|
||||||
/// Chart behavior that monitors the specified [SelectionModel] and darkens the
|
|
||||||
/// color for selected data.
|
|
||||||
///
|
|
||||||
/// This is typically used for bars and pies to highlight segments.
|
|
||||||
///
|
|
||||||
/// It is used in combination with SelectNearest to update the selection model
|
|
||||||
/// and expand selection out to the domain value.
|
|
||||||
class DomainHighlighter<D> implements ChartBehavior<D> {
|
|
||||||
final SelectionModelType selectionModelType;
|
|
||||||
|
|
||||||
BaseChart<D> _chart;
|
|
||||||
|
|
||||||
LifecycleListener<D> _lifecycleListener;
|
|
||||||
|
|
||||||
DomainHighlighter([this.selectionModelType = SelectionModelType.info]) {
|
|
||||||
_lifecycleListener =
|
|
||||||
LifecycleListener<D>(onPostprocess: _updateColorFunctions);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectionChanged(SelectionModel selectionModel) {
|
|
||||||
_chart.redraw(skipLayout: true, skipAnimation: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateColorFunctions(List<MutableSeries<D>> seriesList) {
|
|
||||||
SelectionModel selectionModel =
|
|
||||||
_chart.getSelectionModel(selectionModelType);
|
|
||||||
seriesList.forEach((series) {
|
|
||||||
final origColorFn = series.colorFn;
|
|
||||||
|
|
||||||
if (origColorFn != null) {
|
|
||||||
series.colorFn = (index) {
|
|
||||||
final origColor = origColorFn(index);
|
|
||||||
if (selectionModel.isDatumSelected(series, index)) {
|
|
||||||
return origColor.darker;
|
|
||||||
} else {
|
|
||||||
return origColor;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
_chart = chart;
|
|
||||||
chart.addLifecycleListener(_lifecycleListener);
|
|
||||||
chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.addSelectionChangedListener(_selectionChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart chart) {
|
|
||||||
chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.removeSelectionChangedListener(_selectionChanged);
|
|
||||||
chart.removeLifecycleListener(_lifecycleListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'domainHighlight-${selectionModelType.toString()}';
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
import '../processed_series.dart' show MutableSeries;
|
|
||||||
import '../selection_model/selection_model.dart'
|
|
||||||
show SelectionModel, SelectionModelType;
|
|
||||||
import '../series_datum.dart' show SeriesDatumConfig;
|
|
||||||
import 'chart_behavior.dart' show ChartBehavior;
|
|
||||||
|
|
||||||
/// Behavior that sets initial selection.
|
|
||||||
class InitialSelection<D> implements ChartBehavior<D> {
|
|
||||||
final SelectionModelType selectionModelType;
|
|
||||||
|
|
||||||
/// List of series id of initially selected series.
|
|
||||||
final List<String> selectedSeriesConfig;
|
|
||||||
|
|
||||||
/// List of [SeriesDatumConfig] that represents the initially selected datums.
|
|
||||||
final List<SeriesDatumConfig> selectedDataConfig;
|
|
||||||
|
|
||||||
BaseChart<D> _chart;
|
|
||||||
LifecycleListener<D> _lifecycleListener;
|
|
||||||
bool _firstDraw = true;
|
|
||||||
|
|
||||||
// TODO : When the series changes, if the user does not also
|
|
||||||
// change the index the wrong item could be highlighted.
|
|
||||||
InitialSelection(
|
|
||||||
{this.selectionModelType = SelectionModelType.info,
|
|
||||||
this.selectedDataConfig,
|
|
||||||
this.selectedSeriesConfig}) {
|
|
||||||
_lifecycleListener = LifecycleListener<D>(onData: _setInitialSelection);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setInitialSelection(List<MutableSeries<D>> seriesList) {
|
|
||||||
if (!_firstDraw) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_firstDraw = false;
|
|
||||||
|
|
||||||
final immutableModel = SelectionModel<D>.fromConfig(
|
|
||||||
selectedDataConfig, selectedSeriesConfig, seriesList);
|
|
||||||
|
|
||||||
_chart.getSelectionModel(selectionModelType).updateSelection(
|
|
||||||
immutableModel.selectedDatum, immutableModel.selectedSeries,
|
|
||||||
notifyListeners: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
_chart = chart;
|
|
||||||
chart.addLifecycleListener(_lifecycleListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart<D> chart) {
|
|
||||||
chart.removeLifecycleListener(_lifecycleListener);
|
|
||||||
_chart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'InitialSelection-${selectionModelType.toString()}}';
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
|
|
||||||
import '../../datum_details.dart' show MeasureFormatter;
|
|
||||||
import '../../selection_model/selection_model.dart' show SelectionModelType;
|
|
||||||
import 'legend.dart';
|
|
||||||
import 'legend_entry_generator.dart';
|
|
||||||
import 'per_datum_legend_entry_generator.dart';
|
|
||||||
|
|
||||||
/// Datum legend behavior for charts.
|
|
||||||
///
|
|
||||||
/// By default this behavior creates one legend entry per datum in the first
|
|
||||||
/// series rendered on the chart.
|
|
||||||
///
|
|
||||||
/// TODO: Allows for hovering over a datum in legend to highlight
|
|
||||||
/// corresponding datum in draw area.
|
|
||||||
///
|
|
||||||
/// TODO: Implement tap to hide individual data in the series.
|
|
||||||
class DatumLegend<D> extends Legend<D> {
|
|
||||||
/// Whether or not the series legend should show measures on datum selection.
|
|
||||||
bool _showMeasures;
|
|
||||||
|
|
||||||
DatumLegend({
|
|
||||||
SelectionModelType selectionModelType,
|
|
||||||
LegendEntryGenerator<D> legendEntryGenerator,
|
|
||||||
MeasureFormatter measureFormatter,
|
|
||||||
MeasureFormatter secondaryMeasureFormatter,
|
|
||||||
bool showMeasures,
|
|
||||||
LegendDefaultMeasure legendDefaultMeasure,
|
|
||||||
TextStyleSpec entryTextStyle,
|
|
||||||
}) : super(
|
|
||||||
selectionModelType: selectionModelType ?? SelectionModelType.info,
|
|
||||||
legendEntryGenerator:
|
|
||||||
legendEntryGenerator ?? PerDatumLegendEntryGenerator(),
|
|
||||||
entryTextStyle: entryTextStyle) {
|
|
||||||
// Call the setters that include the setting for default.
|
|
||||||
this.showMeasures = showMeasures;
|
|
||||||
this.legendDefaultMeasure = legendDefaultMeasure;
|
|
||||||
this.measureFormatter = measureFormatter;
|
|
||||||
this.secondaryMeasureFormatter = secondaryMeasureFormatter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not the legend should show measures.
|
|
||||||
///
|
|
||||||
/// By default this is false, measures are not shown. When set to true, the
|
|
||||||
/// default behavior is to show measure only if there is selected data.
|
|
||||||
/// Please set [legendDefaultMeasure] to something other than none to enable
|
|
||||||
/// showing measures when there is no selection.
|
|
||||||
///
|
|
||||||
/// If [showMeasure] is set to null, it is changed to the default of false.
|
|
||||||
bool get showMeasures => _showMeasures;
|
|
||||||
|
|
||||||
set showMeasures(bool showMeasures) {
|
|
||||||
_showMeasures = showMeasures ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Option to show measures when selection is null.
|
|
||||||
///
|
|
||||||
/// By default this is set to none, so no measures are shown when there is
|
|
||||||
/// no selection.
|
|
||||||
///
|
|
||||||
/// If [legendDefaultMeasure] is set to null, it is changed to the default of
|
|
||||||
/// none.
|
|
||||||
LegendDefaultMeasure get legendDefaultMeasure =>
|
|
||||||
legendEntryGenerator.legendDefaultMeasure;
|
|
||||||
|
|
||||||
set legendDefaultMeasure(LegendDefaultMeasure legendDefaultMeasure) {
|
|
||||||
legendEntryGenerator.legendDefaultMeasure =
|
|
||||||
legendDefaultMeasure ?? LegendDefaultMeasure.none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formatter for measure values.
|
|
||||||
///
|
|
||||||
/// This is optional. The default formatter formats measure values with
|
|
||||||
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
|
|
||||||
/// returned.
|
|
||||||
set measureFormatter(MeasureFormatter formatter) {
|
|
||||||
legendEntryGenerator.measureFormatter =
|
|
||||||
formatter ?? defaultLegendMeasureFormatter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formatter for measure values of series that uses the secondary axis.
|
|
||||||
///
|
|
||||||
/// This is optional. The default formatter formats measure values with
|
|
||||||
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
|
|
||||||
/// returned.
|
|
||||||
set secondaryMeasureFormatter(MeasureFormatter formatter) {
|
|
||||||
legendEntryGenerator.secondaryMeasureFormatter =
|
|
||||||
formatter ?? defaultLegendMeasureFormatter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show Rectangle;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show protected;
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
|
|
||||||
import '../../../layout/layout_view.dart'
|
|
||||||
show
|
|
||||||
LayoutPosition,
|
|
||||||
LayoutView,
|
|
||||||
LayoutViewConfig,
|
|
||||||
LayoutViewPositionOrder,
|
|
||||||
LayoutViewPaintOrder,
|
|
||||||
ViewMeasuredSizes;
|
|
||||||
import '../../base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
import '../../chart_canvas.dart' show ChartCanvas;
|
|
||||||
import '../../chart_context.dart' show ChartContext;
|
|
||||||
import '../../processed_series.dart' show MutableSeries;
|
|
||||||
import '../../selection_model/selection_model.dart'
|
|
||||||
show SelectionModel, SelectionModelType;
|
|
||||||
import '../chart_behavior.dart'
|
|
||||||
show
|
|
||||||
BehaviorPosition,
|
|
||||||
ChartBehavior,
|
|
||||||
InsideJustification,
|
|
||||||
OutsideJustification;
|
|
||||||
import 'legend_entry.dart';
|
|
||||||
import 'legend_entry_generator.dart';
|
|
||||||
|
|
||||||
/// Legend behavior for charts.
|
|
||||||
///
|
|
||||||
/// Since legends are desired to be customizable, building and displaying the
|
|
||||||
/// visual content of legends is done on the native platforms. This allows users
|
|
||||||
/// to specify customized content for legends using the native platform (ex. for
|
|
||||||
/// Flutter, using widgets).
|
|
||||||
abstract class Legend<D> implements ChartBehavior<D>, LayoutView {
|
|
||||||
final SelectionModelType selectionModelType;
|
|
||||||
final legendState = LegendState<D>();
|
|
||||||
final LegendEntryGenerator<D> legendEntryGenerator;
|
|
||||||
|
|
||||||
/// Sets title text to display before legend entries.
|
|
||||||
String title;
|
|
||||||
|
|
||||||
BaseChart _chart;
|
|
||||||
LifecycleListener<D> _lifecycleListener;
|
|
||||||
|
|
||||||
Rectangle<int> _componentBounds;
|
|
||||||
Rectangle<int> _drawAreaBounds;
|
|
||||||
GraphicsFactory graphicsFactory;
|
|
||||||
|
|
||||||
BehaviorPosition behaviorPosition = BehaviorPosition.end;
|
|
||||||
OutsideJustification outsideJustification =
|
|
||||||
OutsideJustification.startDrawArea;
|
|
||||||
InsideJustification insideJustification = InsideJustification.topStart;
|
|
||||||
LegendCellPadding cellPadding;
|
|
||||||
LegendCellPadding legendPadding;
|
|
||||||
|
|
||||||
/// Text style of the legend title text.
|
|
||||||
TextStyleSpec titleTextStyle;
|
|
||||||
|
|
||||||
/// Configures the behavior of the legend when the user taps/clicks on an
|
|
||||||
/// entry. Defaults to no behavior.
|
|
||||||
///
|
|
||||||
/// Tapping on a legend entry will update the data visible on the chart. For
|
|
||||||
/// example, when [LegendTapHandling.hide] is configured, the series or datum
|
|
||||||
/// associated with that entry will be removed from the chart. Tapping on that
|
|
||||||
/// entry a second time will make the data visible again.
|
|
||||||
LegendTapHandling legendTapHandling = LegendTapHandling.hide;
|
|
||||||
|
|
||||||
List<MutableSeries<D>> _currentSeriesList;
|
|
||||||
|
|
||||||
/// Save this in order to check if series list have changed and regenerate
|
|
||||||
/// the legend entries.
|
|
||||||
List<MutableSeries<D>> _postProcessSeriesList;
|
|
||||||
|
|
||||||
static final _decimalPattern = NumberFormat.decimalPattern();
|
|
||||||
|
|
||||||
/// Default measure formatter for legends.
|
|
||||||
@protected
|
|
||||||
String defaultLegendMeasureFormatter(num value) {
|
|
||||||
return (value == null) ? '' : _decimalPattern.format(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Legend({this.selectionModelType, this.legendEntryGenerator, entryTextStyle}) {
|
|
||||||
_lifecycleListener = LifecycleListener(
|
|
||||||
onPostprocess: _postProcess, onPreprocess: _preProcess, onData: onData);
|
|
||||||
legendEntryGenerator.entryTextStyle = entryTextStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Text style of the legend entry text.
|
|
||||||
TextStyleSpec get entryTextStyle => legendEntryGenerator.entryTextStyle;
|
|
||||||
|
|
||||||
set entryTextStyle(TextStyleSpec entryTextStyle) {
|
|
||||||
legendEntryGenerator.entryTextStyle = entryTextStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets any hidden series data when new data is drawn on the chart.
|
|
||||||
@protected
|
|
||||||
void onData(List<MutableSeries<D>> seriesList) {}
|
|
||||||
|
|
||||||
/// Store off a copy of the series list for use when we render the legend.
|
|
||||||
void _preProcess(List<MutableSeries<D>> seriesList) {
|
|
||||||
_currentSeriesList = List.from(seriesList);
|
|
||||||
preProcessSeriesList(seriesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Overridable method that may be used by concrete [Legend] instances to
|
|
||||||
/// manipulate the series list.
|
|
||||||
@protected
|
|
||||||
void preProcessSeriesList(List<MutableSeries<D>> seriesList) {}
|
|
||||||
|
|
||||||
/// Build LegendEntries from list of series.
|
|
||||||
void _postProcess(List<MutableSeries<D>> seriesList) {
|
|
||||||
// Get the selection model directly from chart on post process.
|
|
||||||
//
|
|
||||||
// This is because if initial selection is set as a behavior, it will be
|
|
||||||
// handled during onData. onData is prior to this behavior's postProcess
|
|
||||||
// call, so the selection will have changed prior to the entries being
|
|
||||||
// generated.
|
|
||||||
final selectionModel = chart.getSelectionModel(selectionModelType);
|
|
||||||
|
|
||||||
// Update entries if the selection model is different because post
|
|
||||||
// process is called on each draw cycle, so this is called on each animation
|
|
||||||
// frame and we don't want to update and request the native platform to
|
|
||||||
// rebuild if nothing has changed.
|
|
||||||
//
|
|
||||||
// Also update legend entries if the series list has changed.
|
|
||||||
if (legendState._selectionModel != selectionModel ||
|
|
||||||
_postProcessSeriesList != seriesList) {
|
|
||||||
legendState._legendEntries =
|
|
||||||
legendEntryGenerator.getLegendEntries(_currentSeriesList);
|
|
||||||
|
|
||||||
legendState._selectionModel = selectionModel;
|
|
||||||
_postProcessSeriesList = seriesList;
|
|
||||||
_updateLegendEntries();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to handle when series data changes, selection should be reset
|
|
||||||
|
|
||||||
/// Update the legend state with [selectionModel] and request legend update.
|
|
||||||
void _selectionChanged(SelectionModel selectionModel) {
|
|
||||||
legendState._selectionModel = selectionModel;
|
|
||||||
_updateLegendEntries();
|
|
||||||
}
|
|
||||||
|
|
||||||
ChartContext get chartContext => _chart.context;
|
|
||||||
|
|
||||||
/// Internally update legend entries, before calling [updateLegend] that
|
|
||||||
/// notifies the native platform.
|
|
||||||
void _updateLegendEntries() {
|
|
||||||
legendEntryGenerator.updateLegendEntries(legendState._legendEntries,
|
|
||||||
legendState._selectionModel, chart.currentSeriesList);
|
|
||||||
|
|
||||||
updateLegend();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Requires override to show in native platform
|
|
||||||
void updateLegend() {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
_chart = chart;
|
|
||||||
chart.addLifecycleListener(_lifecycleListener);
|
|
||||||
chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.addSelectionChangedListener(_selectionChanged);
|
|
||||||
|
|
||||||
chart.addView(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart chart) {
|
|
||||||
chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.removeSelectionChangedListener(_selectionChanged);
|
|
||||||
chart.removeLifecycleListener(_lifecycleListener);
|
|
||||||
|
|
||||||
chart.removeView(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
BaseChart get chart => _chart;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'legend-${selectionModelType.toString()}';
|
|
||||||
|
|
||||||
bool get isRtl => _chart.context.isRtl;
|
|
||||||
|
|
||||||
@override
|
|
||||||
LayoutViewConfig get layoutConfig {
|
|
||||||
return LayoutViewConfig(
|
|
||||||
position: _layoutPosition,
|
|
||||||
positionOrder: LayoutViewPositionOrder.legend,
|
|
||||||
paintOrder: LayoutViewPaintOrder.legend);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get layout position from legend position.
|
|
||||||
LayoutPosition get _layoutPosition {
|
|
||||||
LayoutPosition position;
|
|
||||||
switch (behaviorPosition) {
|
|
||||||
case BehaviorPosition.bottom:
|
|
||||||
position = LayoutPosition.Bottom;
|
|
||||||
break;
|
|
||||||
case BehaviorPosition.end:
|
|
||||||
position = isRtl ? LayoutPosition.Left : LayoutPosition.Right;
|
|
||||||
break;
|
|
||||||
case BehaviorPosition.inside:
|
|
||||||
position = LayoutPosition.DrawArea;
|
|
||||||
break;
|
|
||||||
case BehaviorPosition.start:
|
|
||||||
position = isRtl ? LayoutPosition.Right : LayoutPosition.Left;
|
|
||||||
position = isRtl ? LayoutPosition.Right : LayoutPosition.Left;
|
|
||||||
break;
|
|
||||||
case BehaviorPosition.top:
|
|
||||||
position = LayoutPosition.Top;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
|
|
||||||
// Native child classes should override this method to return real
|
|
||||||
// measurements.
|
|
||||||
return ViewMeasuredSizes(preferredWidth: 0, preferredHeight: 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
|
|
||||||
_componentBounds = componentBounds;
|
|
||||||
_drawAreaBounds = drawAreaBounds;
|
|
||||||
|
|
||||||
updateLegend();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(ChartCanvas canvas, double animationPercent) {}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Rectangle<int> get componentBounds => _componentBounds;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isSeriesRenderer => false;
|
|
||||||
|
|
||||||
// Gets the draw area bounds for native legend content to position itself
|
|
||||||
// accordingly.
|
|
||||||
Rectangle<int> get drawAreaBounds => _drawAreaBounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores legend data used by native legend content builder.
|
|
||||||
class LegendState<D> {
|
|
||||||
List<LegendEntry<D>> _legendEntries;
|
|
||||||
SelectionModel _selectionModel;
|
|
||||||
|
|
||||||
List<LegendEntry<D>> get legendEntries => _legendEntries;
|
|
||||||
SelectionModel get selectionModel => _selectionModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores legend cell padding, in percents or pixels.
|
|
||||||
///
|
|
||||||
/// If a percent is specified, it takes precedence over a flat pixel value.
|
|
||||||
class LegendCellPadding {
|
|
||||||
final double bottomPct;
|
|
||||||
final double bottomPx;
|
|
||||||
final double leftPct;
|
|
||||||
final double leftPx;
|
|
||||||
final double rightPct;
|
|
||||||
final double rightPx;
|
|
||||||
final double topPct;
|
|
||||||
final double topPx;
|
|
||||||
|
|
||||||
/// Creates padding in percents from the left, top, right, and bottom.
|
|
||||||
const LegendCellPadding.fromLTRBPct(
|
|
||||||
this.leftPct, this.topPct, this.rightPct, this.bottomPct)
|
|
||||||
: leftPx = null,
|
|
||||||
topPx = null,
|
|
||||||
rightPx = null,
|
|
||||||
bottomPx = null;
|
|
||||||
|
|
||||||
/// Creates padding in pixels from the left, top, right, and bottom.
|
|
||||||
const LegendCellPadding.fromLTRBPx(
|
|
||||||
this.leftPx, this.topPx, this.rightPx, this.bottomPx)
|
|
||||||
: leftPct = null,
|
|
||||||
topPct = null,
|
|
||||||
rightPct = null,
|
|
||||||
bottomPct = null;
|
|
||||||
|
|
||||||
/// Creates padding in percents from the top, right, bottom, and left.
|
|
||||||
const LegendCellPadding.fromTRBLPct(
|
|
||||||
this.topPct, this.rightPct, this.bottomPct, this.leftPct)
|
|
||||||
: topPx = null,
|
|
||||||
rightPx = null,
|
|
||||||
bottomPx = null,
|
|
||||||
leftPx = null;
|
|
||||||
|
|
||||||
/// Creates padding in pixels from the top, right, bottom, and left.
|
|
||||||
const LegendCellPadding.fromTRBLPx(
|
|
||||||
this.topPx, this.rightPx, this.bottomPx, this.leftPx)
|
|
||||||
: topPct = null,
|
|
||||||
rightPct = null,
|
|
||||||
bottomPct = null,
|
|
||||||
leftPct = null;
|
|
||||||
|
|
||||||
/// Creates cell padding where all the offsets are `value` in percent.
|
|
||||||
///
|
|
||||||
/// ## Sample code
|
|
||||||
///
|
|
||||||
/// Typical eight percent margin on all sides:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// const LegendCellPadding.allPct(8.0)
|
|
||||||
/// ```
|
|
||||||
const LegendCellPadding.allPct(double value)
|
|
||||||
: leftPct = value,
|
|
||||||
topPct = value,
|
|
||||||
rightPct = value,
|
|
||||||
bottomPct = value,
|
|
||||||
leftPx = null,
|
|
||||||
topPx = null,
|
|
||||||
rightPx = null,
|
|
||||||
bottomPx = null;
|
|
||||||
|
|
||||||
/// Creates cell padding where all the offsets are `value` in pixels.
|
|
||||||
///
|
|
||||||
/// ## Sample code
|
|
||||||
///
|
|
||||||
/// Typical eight-pixel margin on all sides:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// const LegendCellPadding.allPx(8.0)
|
|
||||||
/// ```
|
|
||||||
const LegendCellPadding.allPx(double value)
|
|
||||||
: leftPx = value,
|
|
||||||
topPx = value,
|
|
||||||
rightPx = value,
|
|
||||||
bottomPx = value,
|
|
||||||
leftPct = null,
|
|
||||||
topPct = null,
|
|
||||||
rightPct = null,
|
|
||||||
bottomPct = null;
|
|
||||||
|
|
||||||
double bottom(num height) =>
|
|
||||||
bottomPct != null ? bottomPct * height : bottomPx;
|
|
||||||
|
|
||||||
double left(num width) => leftPct != null ? leftPct * width : leftPx;
|
|
||||||
|
|
||||||
double right(num width) => rightPct != null ? rightPct * width : rightPx;
|
|
||||||
|
|
||||||
double top(num height) => topPct != null ? topPct * height : topPx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Options for behavior of tapping/clicking on entries in the legend.
|
|
||||||
enum LegendTapHandling {
|
|
||||||
/// No associated behavior.
|
|
||||||
none,
|
|
||||||
|
|
||||||
/// Hide elements on the chart associated with this legend entry.
|
|
||||||
hide,
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../../common/color.dart';
|
|
||||||
import '../../../../common/symbol_renderer.dart';
|
|
||||||
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
|
|
||||||
import '../../processed_series.dart' show ImmutableSeries;
|
|
||||||
import '../../series_renderer.dart' show rendererKey;
|
|
||||||
|
|
||||||
/// Holder for the information used for a legend row.
|
|
||||||
///
|
|
||||||
/// [T] the datum class type for the series passed in.
|
|
||||||
/// [D] the domain class type for the datum.
|
|
||||||
class LegendEntry<D> {
|
|
||||||
final String label;
|
|
||||||
final ImmutableSeries<D> series;
|
|
||||||
final dynamic datum;
|
|
||||||
final int datumIndex;
|
|
||||||
final D domain;
|
|
||||||
final Color color;
|
|
||||||
final TextStyleSpec textStyle;
|
|
||||||
double value;
|
|
||||||
String formattedValue;
|
|
||||||
bool isSelected;
|
|
||||||
|
|
||||||
/// Zero based index for the row where this legend appears in the legend.
|
|
||||||
int rowNumber;
|
|
||||||
|
|
||||||
/// Zero based index for the column where this legend appears in the legend.
|
|
||||||
int columnNumber;
|
|
||||||
|
|
||||||
/// Total number of rows in the legend.
|
|
||||||
int rowCount;
|
|
||||||
|
|
||||||
/// Total number of columns in the legend.
|
|
||||||
int columnCount;
|
|
||||||
|
|
||||||
/// Indicates whether this is in the first row of a tabular layout.
|
|
||||||
bool inFirstRow;
|
|
||||||
|
|
||||||
/// Indicates whether this is in the first column of a tabular layout.
|
|
||||||
bool inFirstColumn;
|
|
||||||
|
|
||||||
/// Indicates whether this is in the last row of a tabular layout.
|
|
||||||
bool inLastRow;
|
|
||||||
|
|
||||||
/// Indicates whether this is in the last column of a tabular layout.
|
|
||||||
bool inLastColumn;
|
|
||||||
|
|
||||||
// TODO: Forward the default formatters from series and allow for
|
|
||||||
// native legends to provide separate formatters.
|
|
||||||
|
|
||||||
LegendEntry(this.series, this.label,
|
|
||||||
{this.datum,
|
|
||||||
this.datumIndex,
|
|
||||||
this.domain,
|
|
||||||
this.value,
|
|
||||||
this.color,
|
|
||||||
this.textStyle,
|
|
||||||
this.isSelected = false,
|
|
||||||
this.rowNumber,
|
|
||||||
this.columnNumber,
|
|
||||||
this.rowCount,
|
|
||||||
this.columnCount,
|
|
||||||
this.inFirstRow,
|
|
||||||
this.inFirstColumn,
|
|
||||||
this.inLastRow,
|
|
||||||
this.inLastColumn});
|
|
||||||
|
|
||||||
/// Get the native symbol renderer stored in the series.
|
|
||||||
SymbolRenderer get symbolRenderer =>
|
|
||||||
series.getAttr(rendererKey).symbolRenderer;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
|
|
||||||
import '../../datum_details.dart' show MeasureFormatter;
|
|
||||||
import '../../processed_series.dart' show MutableSeries;
|
|
||||||
import '../../selection_model/selection_model.dart';
|
|
||||||
import 'legend_entry.dart';
|
|
||||||
|
|
||||||
/// A strategy for generating a list of [LegendEntry] based on the series drawn.
|
|
||||||
///
|
|
||||||
/// [D] the domain class type for the datum.
|
|
||||||
abstract class LegendEntryGenerator<D> {
|
|
||||||
/// Generates a list of legend entries based on the series drawn on the chart.
|
|
||||||
///
|
|
||||||
/// [seriesList] Processed series list.
|
|
||||||
List<LegendEntry<D>> getLegendEntries(List<MutableSeries<D>> seriesList);
|
|
||||||
|
|
||||||
/// Update the list of legend entries based on the selection model.
|
|
||||||
///
|
|
||||||
/// [legendEntries] Existing legend entries to update.
|
|
||||||
/// [selectionModel] Selection model to query selected state.
|
|
||||||
/// [seriesList] Processed series list.
|
|
||||||
void updateLegendEntries(List<LegendEntry<D>> legendEntries,
|
|
||||||
SelectionModel<D> selectionModel, List<MutableSeries<D>> seriesList);
|
|
||||||
|
|
||||||
MeasureFormatter get measureFormatter;
|
|
||||||
|
|
||||||
set measureFormatter(MeasureFormatter formatter);
|
|
||||||
|
|
||||||
MeasureFormatter get secondaryMeasureFormatter;
|
|
||||||
|
|
||||||
set secondaryMeasureFormatter(MeasureFormatter formatter);
|
|
||||||
|
|
||||||
LegendDefaultMeasure get legendDefaultMeasure;
|
|
||||||
|
|
||||||
set legendDefaultMeasure(LegendDefaultMeasure noSelectionMeasure);
|
|
||||||
|
|
||||||
TextStyleSpec get entryTextStyle;
|
|
||||||
|
|
||||||
set entryTextStyle(TextStyleSpec entryTextStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Options for calculating what measures are shown when there is no selection.
|
|
||||||
enum LegendDefaultMeasure {
|
|
||||||
// No measures are shown where there is no selection.
|
|
||||||
none,
|
|
||||||
// Sum of all measure values for the series.
|
|
||||||
sum,
|
|
||||||
// Average of all measure values for the series.
|
|
||||||
average,
|
|
||||||
// The first measure value of the series.
|
|
||||||
firstValue,
|
|
||||||
// The last measure value of the series.
|
|
||||||
lastValue,
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
//import 'dart:collection' show HashSet;
|
|
||||||
import '../../../cartesian/axis/axis.dart' show Axis, measureAxisIdKey;
|
|
||||||
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
|
|
||||||
import '../../datum_details.dart' show MeasureFormatter;
|
|
||||||
import '../../processed_series.dart' show ImmutableSeries, MutableSeries;
|
|
||||||
import '../../selection_model/selection_model.dart';
|
|
||||||
import 'legend_entry.dart';
|
|
||||||
import 'legend_entry_generator.dart';
|
|
||||||
|
|
||||||
/// A strategy for generating a list of [LegendEntry] per series data drawn.
|
|
||||||
///
|
|
||||||
/// [D] the domain class type for the datum.
|
|
||||||
class PerDatumLegendEntryGenerator<D> implements LegendEntryGenerator<D> {
|
|
||||||
TextStyleSpec entryTextStyle;
|
|
||||||
MeasureFormatter measureFormatter;
|
|
||||||
MeasureFormatter secondaryMeasureFormatter;
|
|
||||||
|
|
||||||
/// Option for showing measures when there is no selection.
|
|
||||||
LegendDefaultMeasure legendDefaultMeasure;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<LegendEntry<D>> getLegendEntries(List<MutableSeries<D>> seriesList) {
|
|
||||||
final legendEntries = <LegendEntry<D>>[];
|
|
||||||
|
|
||||||
final series = seriesList[0];
|
|
||||||
for (var i = 0; i < series.data.length; i++) {
|
|
||||||
legendEntries.add(LegendEntry<D>(series, series.domainFn(i).toString(),
|
|
||||||
color: series.colorFn(i),
|
|
||||||
datum: series.data[i],
|
|
||||||
datumIndex: i,
|
|
||||||
textStyle: entryTextStyle));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update with measures only if showing measure on no selection.
|
|
||||||
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
|
|
||||||
_updateFromSeriesList(legendEntries, seriesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
return legendEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateLegendEntries(List<LegendEntry<D>> legendEntries,
|
|
||||||
SelectionModel<D> selectionModel, List<MutableSeries<D>> seriesList) {
|
|
||||||
if (selectionModel.hasAnySelection) {
|
|
||||||
_updateFromSelection(legendEntries, selectionModel);
|
|
||||||
} else {
|
|
||||||
// Update with measures only if showing measure on no selection.
|
|
||||||
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
|
|
||||||
_updateFromSeriesList(legendEntries, seriesList);
|
|
||||||
} else {
|
|
||||||
_resetLegendEntryMeasures(legendEntries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update legend entries with measures of the selected datum
|
|
||||||
void _updateFromSelection(
|
|
||||||
List<LegendEntry<D>> legendEntries, SelectionModel<D> selectionModel) {
|
|
||||||
// Given that each legend entry only has one datum associated with it, any
|
|
||||||
// option for [legendDefaultMeasure] essentially boils down to just showing
|
|
||||||
// the measure value.
|
|
||||||
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
|
|
||||||
for (var entry in legendEntries) {
|
|
||||||
final series = entry.series;
|
|
||||||
final measure = series.measureFn(entry.datumIndex);
|
|
||||||
entry.value = measure.toDouble();
|
|
||||||
entry.formattedValue = _getFormattedMeasureValue(series, measure);
|
|
||||||
|
|
||||||
entry.isSelected = selectionModel.selectedSeries
|
|
||||||
.any((selectedSeries) => series.id == selectedSeries.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _resetLegendEntryMeasures(List<LegendEntry<D>> legendEntries) {
|
|
||||||
for (LegendEntry<D> entry in legendEntries) {
|
|
||||||
entry.value = null;
|
|
||||||
entry.formattedValue = null;
|
|
||||||
entry.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update each legend entry by calculating measure values in [seriesList].
|
|
||||||
///
|
|
||||||
/// This method calculates the legend's measure value to show when there is no
|
|
||||||
/// selection. The type of calculation is based on the [legendDefaultMeasure]
|
|
||||||
/// value.
|
|
||||||
void _updateFromSeriesList(
|
|
||||||
List<LegendEntry<D>> legendEntries, List<MutableSeries<D>> seriesList) {
|
|
||||||
// Given that each legend entry only has one datum associated with it, any
|
|
||||||
// option for [legendDefaultMeasure] essentially boils down to just showing
|
|
||||||
// the measure value.
|
|
||||||
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
|
|
||||||
for (var entry in legendEntries) {
|
|
||||||
final series = entry.series;
|
|
||||||
final measure = series.measureFn(entry.datumIndex);
|
|
||||||
entry.value = measure.toDouble();
|
|
||||||
entry.formattedValue = _getFormattedMeasureValue(series, measure);
|
|
||||||
entry.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formats the measure value using the appropriate measure formatter
|
|
||||||
/// function for the series.
|
|
||||||
String _getFormattedMeasureValue(ImmutableSeries series, num measure) {
|
|
||||||
return (series.getAttr(measureAxisIdKey) == Axis.secondaryMeasureAxisId)
|
|
||||||
? secondaryMeasureFormatter(measure)
|
|
||||||
: measureFormatter(measure);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is PerDatumLegendEntryGenerator &&
|
|
||||||
measureFormatter == other.measureFormatter &&
|
|
||||||
secondaryMeasureFormatter == other.secondaryMeasureFormatter &&
|
|
||||||
legendDefaultMeasure == other.legendDefaultMeasure &&
|
|
||||||
entryTextStyle == other.entryTextStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = measureFormatter?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + secondaryMeasureFormatter.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + legendDefaultMeasure.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + entryTextStyle.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:collection' show HashSet;
|
|
||||||
|
|
||||||
import '../../../cartesian/axis/axis.dart' show Axis, measureAxisIdKey;
|
|
||||||
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
|
|
||||||
import '../../datum_details.dart' show MeasureFormatter;
|
|
||||||
import '../../processed_series.dart' show MutableSeries;
|
|
||||||
import '../../selection_model/selection_model.dart';
|
|
||||||
import '../../series_datum.dart' show SeriesDatum;
|
|
||||||
import 'legend_entry.dart';
|
|
||||||
import 'legend_entry_generator.dart';
|
|
||||||
|
|
||||||
/// A strategy for generating a list of [LegendEntry] per series drawn.
|
|
||||||
///
|
|
||||||
/// [D] the domain class type for the datum.
|
|
||||||
class PerSeriesLegendEntryGenerator<D> implements LegendEntryGenerator<D> {
|
|
||||||
TextStyleSpec entryTextStyle;
|
|
||||||
MeasureFormatter measureFormatter;
|
|
||||||
MeasureFormatter secondaryMeasureFormatter;
|
|
||||||
|
|
||||||
/// Option for showing measures when there is no selection.
|
|
||||||
LegendDefaultMeasure legendDefaultMeasure;
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<LegendEntry<D>> getLegendEntries(List<MutableSeries<D>> seriesList) {
|
|
||||||
final legendEntries = seriesList
|
|
||||||
.map((series) => LegendEntry<D>(series, series.displayName,
|
|
||||||
color: series.colorFn(0), textStyle: entryTextStyle))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
// Update with measures only if showing measure on no selection.
|
|
||||||
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
|
|
||||||
_updateFromSeriesList(legendEntries, seriesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
return legendEntries;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void updateLegendEntries(List<LegendEntry<D>> legendEntries,
|
|
||||||
SelectionModel<D> selectionModel, List<MutableSeries<D>> seriesList) {
|
|
||||||
if (selectionModel.hasAnySelection) {
|
|
||||||
_updateFromSelection(legendEntries, selectionModel);
|
|
||||||
} else {
|
|
||||||
// Update with measures only if showing measure on no selection.
|
|
||||||
if (legendDefaultMeasure != LegendDefaultMeasure.none) {
|
|
||||||
_updateFromSeriesList(legendEntries, seriesList);
|
|
||||||
} else {
|
|
||||||
_resetLegendEntryMeasures(legendEntries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update legend entries with measures of the selected datum
|
|
||||||
void _updateFromSelection(
|
|
||||||
List<LegendEntry<D>> legendEntries, SelectionModel<D> selectionModel) {
|
|
||||||
// Map of series ID to the total selected measure value for that series.
|
|
||||||
final seriesAndMeasure = <String, num>{};
|
|
||||||
|
|
||||||
// Hash set of series ID's that use the secondary measure axis
|
|
||||||
final secondaryAxisSeriesIDs = HashSet<String>();
|
|
||||||
|
|
||||||
for (SeriesDatum<D> selectedDatum in selectionModel.selectedDatum) {
|
|
||||||
final series = selectedDatum.series;
|
|
||||||
final seriesId = series.id;
|
|
||||||
final measure = series.measureFn(selectedDatum.index) ?? 0;
|
|
||||||
|
|
||||||
seriesAndMeasure[seriesId] = seriesAndMeasure.containsKey(seriesId)
|
|
||||||
? seriesAndMeasure[seriesId] + measure
|
|
||||||
: measure;
|
|
||||||
|
|
||||||
if (series.getAttr(measureAxisIdKey) == Axis.secondaryMeasureAxisId) {
|
|
||||||
secondaryAxisSeriesIDs.add(seriesId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var entry in legendEntries) {
|
|
||||||
final seriesId = entry.series.id;
|
|
||||||
final measureValue = seriesAndMeasure[seriesId]?.toDouble();
|
|
||||||
final formattedValue = secondaryAxisSeriesIDs.contains(seriesId)
|
|
||||||
? secondaryMeasureFormatter(measureValue)
|
|
||||||
: measureFormatter(measureValue);
|
|
||||||
|
|
||||||
entry.value = measureValue;
|
|
||||||
entry.formattedValue = formattedValue;
|
|
||||||
entry.isSelected = selectionModel.selectedSeries
|
|
||||||
.any((selectedSeries) => entry.series.id == selectedSeries.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _resetLegendEntryMeasures(List<LegendEntry<D>> legendEntries) {
|
|
||||||
for (LegendEntry<D> entry in legendEntries) {
|
|
||||||
entry.value = null;
|
|
||||||
entry.formattedValue = null;
|
|
||||||
entry.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update each legend entry by calculating measure values in [seriesList].
|
|
||||||
///
|
|
||||||
/// This method calculates the legend's measure value to show when there is no
|
|
||||||
/// selection. The type of calculation is based on the [legendDefaultMeasure]
|
|
||||||
/// value.
|
|
||||||
void _updateFromSeriesList(
|
|
||||||
List<LegendEntry<D>> legendEntries, List<MutableSeries<D>> seriesList) {
|
|
||||||
// Helper function to sum up the measure values
|
|
||||||
num getMeasureTotal(MutableSeries<D> series) {
|
|
||||||
var measureTotal = 0.0;
|
|
||||||
for (var i = 0; i < series.data.length; i++) {
|
|
||||||
measureTotal += series.measureFn(i);
|
|
||||||
}
|
|
||||||
return measureTotal;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map of series ID to the calculated measure for that series.
|
|
||||||
final seriesAndMeasure = <String, double>{};
|
|
||||||
// Map of series ID and the formatted measure for that series.
|
|
||||||
final seriesAndFormattedMeasure = <String, String>{};
|
|
||||||
|
|
||||||
for (MutableSeries<D> series in seriesList) {
|
|
||||||
final seriesId = series.id;
|
|
||||||
num calculatedMeasure;
|
|
||||||
|
|
||||||
switch (legendDefaultMeasure) {
|
|
||||||
case LegendDefaultMeasure.sum:
|
|
||||||
calculatedMeasure = getMeasureTotal(series);
|
|
||||||
break;
|
|
||||||
case LegendDefaultMeasure.average:
|
|
||||||
calculatedMeasure = getMeasureTotal(series) / series.data.length;
|
|
||||||
break;
|
|
||||||
case LegendDefaultMeasure.firstValue:
|
|
||||||
calculatedMeasure = series.measureFn(0);
|
|
||||||
break;
|
|
||||||
case LegendDefaultMeasure.lastValue:
|
|
||||||
calculatedMeasure = series.measureFn(series.data.length - 1);
|
|
||||||
break;
|
|
||||||
case LegendDefaultMeasure.none:
|
|
||||||
// [calculatedMeasure] intentionally left null, since we do not want
|
|
||||||
// to show any measures.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
seriesAndMeasure[seriesId] = calculatedMeasure?.toDouble();
|
|
||||||
seriesAndFormattedMeasure[seriesId] =
|
|
||||||
(series.getAttr(measureAxisIdKey) == Axis.secondaryMeasureAxisId)
|
|
||||||
? secondaryMeasureFormatter(calculatedMeasure)
|
|
||||||
: measureFormatter(calculatedMeasure);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var entry in legendEntries) {
|
|
||||||
final seriesId = entry.series.id;
|
|
||||||
|
|
||||||
entry.value = seriesAndMeasure[seriesId];
|
|
||||||
entry.formattedValue = seriesAndFormattedMeasure[seriesId];
|
|
||||||
entry.isSelected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
return other is PerSeriesLegendEntryGenerator &&
|
|
||||||
measureFormatter == other.measureFormatter &&
|
|
||||||
secondaryMeasureFormatter == other.secondaryMeasureFormatter &&
|
|
||||||
legendDefaultMeasure == other.legendDefaultMeasure &&
|
|
||||||
entryTextStyle == other.entryTextStyle;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = measureFormatter?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + secondaryMeasureFormatter.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + legendDefaultMeasure.hashCode;
|
|
||||||
hashcode = (hashcode * 37) + entryTextStyle.hashCode;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show protected;
|
|
||||||
|
|
||||||
import '../../../cartesian/axis/spec/axis_spec.dart' show TextStyleSpec;
|
|
||||||
import '../../datum_details.dart' show MeasureFormatter;
|
|
||||||
import '../../processed_series.dart' show MutableSeries;
|
|
||||||
import '../../selection_model/selection_model.dart' show SelectionModelType;
|
|
||||||
import 'legend.dart';
|
|
||||||
import 'legend_entry_generator.dart';
|
|
||||||
import 'per_series_legend_entry_generator.dart';
|
|
||||||
|
|
||||||
// TODO: Allows for hovering over a series in legend to highlight
|
|
||||||
// corresponding series in draw area.
|
|
||||||
|
|
||||||
/// Series legend behavior for charts.
|
|
||||||
///
|
|
||||||
/// By default this behavior creates a legend entry per series.
|
|
||||||
class SeriesLegend<D> extends Legend<D> {
|
|
||||||
/// List of currently hidden series, by ID.
|
|
||||||
final _hiddenSeriesList = Set<String>();
|
|
||||||
|
|
||||||
/// List of series IDs that should be hidden by default.
|
|
||||||
List<String> _defaultHiddenSeries;
|
|
||||||
|
|
||||||
/// Whether or not the series legend should show measures on datum selection.
|
|
||||||
bool _showMeasures;
|
|
||||||
|
|
||||||
SeriesLegend({
|
|
||||||
SelectionModelType selectionModelType,
|
|
||||||
LegendEntryGenerator<D> legendEntryGenerator,
|
|
||||||
MeasureFormatter measureFormatter,
|
|
||||||
MeasureFormatter secondaryMeasureFormatter,
|
|
||||||
bool showMeasures,
|
|
||||||
LegendDefaultMeasure legendDefaultMeasure,
|
|
||||||
TextStyleSpec entryTextStyle,
|
|
||||||
}) : super(
|
|
||||||
selectionModelType: selectionModelType ?? SelectionModelType.info,
|
|
||||||
legendEntryGenerator:
|
|
||||||
legendEntryGenerator ?? PerSeriesLegendEntryGenerator(),
|
|
||||||
entryTextStyle: entryTextStyle) {
|
|
||||||
// Call the setters that include the setting for default.
|
|
||||||
this.showMeasures = showMeasures;
|
|
||||||
this.legendDefaultMeasure = legendDefaultMeasure;
|
|
||||||
this.measureFormatter = measureFormatter;
|
|
||||||
this.secondaryMeasureFormatter = secondaryMeasureFormatter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a list of series IDs that should be hidden by default on first chart
|
|
||||||
/// draw.
|
|
||||||
///
|
|
||||||
/// This will also reset the current list of hidden series, filling it in with
|
|
||||||
/// the new default list.
|
|
||||||
set defaultHiddenSeries(List<String> defaultHiddenSeries) {
|
|
||||||
_defaultHiddenSeries = defaultHiddenSeries;
|
|
||||||
|
|
||||||
_hiddenSeriesList.clear();
|
|
||||||
|
|
||||||
if (_defaultHiddenSeries != null) {
|
|
||||||
_defaultHiddenSeries.forEach(hideSeries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a list of series IDs that should be hidden by default on first chart
|
|
||||||
/// draw.
|
|
||||||
List<String> get defaultHiddenSeries => _defaultHiddenSeries;
|
|
||||||
|
|
||||||
/// Whether or not the legend should show measures.
|
|
||||||
///
|
|
||||||
/// By default this is false, measures are not shown. When set to true, the
|
|
||||||
/// default behavior is to show measure only if there is selected data.
|
|
||||||
/// Please set [legendDefaultMeasure] to something other than none to enable
|
|
||||||
/// showing measures when there is no selection.
|
|
||||||
///
|
|
||||||
/// If [showMeasure] is set to null, it is changed to the default of false.
|
|
||||||
bool get showMeasures => _showMeasures;
|
|
||||||
|
|
||||||
set showMeasures(bool showMeasures) {
|
|
||||||
_showMeasures = showMeasures ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Option to show measures when selection is null.
|
|
||||||
///
|
|
||||||
/// By default this is set to none, so no measures are shown when there is
|
|
||||||
/// no selection.
|
|
||||||
///
|
|
||||||
/// If [legendDefaultMeasure] is set to null, it is changed to the default of
|
|
||||||
/// none.
|
|
||||||
LegendDefaultMeasure get legendDefaultMeasure =>
|
|
||||||
legendEntryGenerator.legendDefaultMeasure;
|
|
||||||
|
|
||||||
set legendDefaultMeasure(LegendDefaultMeasure legendDefaultMeasure) {
|
|
||||||
legendEntryGenerator.legendDefaultMeasure =
|
|
||||||
legendDefaultMeasure ?? LegendDefaultMeasure.none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formatter for measure values.
|
|
||||||
///
|
|
||||||
/// This is optional. The default formatter formats measure values with
|
|
||||||
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
|
|
||||||
/// returned.
|
|
||||||
set measureFormatter(MeasureFormatter formatter) {
|
|
||||||
legendEntryGenerator.measureFormatter =
|
|
||||||
formatter ?? defaultLegendMeasureFormatter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Formatter for measure values of series that uses the secondary axis.
|
|
||||||
///
|
|
||||||
/// This is optional. The default formatter formats measure values with
|
|
||||||
/// NumberFormat.decimalPattern. If the measure value is null, a dash is
|
|
||||||
/// returned.
|
|
||||||
set secondaryMeasureFormatter(MeasureFormatter formatter) {
|
|
||||||
legendEntryGenerator.secondaryMeasureFormatter =
|
|
||||||
formatter ?? defaultLegendMeasureFormatter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove series IDs from the currently hidden list if those series have been
|
|
||||||
/// removed from the chart data. The goal is to allow any metric that is
|
|
||||||
/// removed from a chart, and later re-added to it, to be visible to the user.
|
|
||||||
@override
|
|
||||||
void onData(List<MutableSeries<D>> seriesList) {
|
|
||||||
// If a series was removed from the chart, remove it from our current list
|
|
||||||
// of hidden series.
|
|
||||||
final seriesIds = seriesList.map((series) => series.id);
|
|
||||||
|
|
||||||
_hiddenSeriesList.removeWhere((id) => !seriesIds.contains(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void preProcessSeriesList(List<MutableSeries<D>> seriesList) {
|
|
||||||
seriesList.removeWhere((series) {
|
|
||||||
return _hiddenSeriesList.contains(series.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hides the data for a series on the chart by [seriesId].
|
|
||||||
///
|
|
||||||
/// The entry in the legend for this series will be grayed out to indicate
|
|
||||||
/// that it is hidden.
|
|
||||||
@protected
|
|
||||||
void hideSeries(String seriesId) {
|
|
||||||
_hiddenSeriesList.add(seriesId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shows the data for a series on the chart by [seriesId].
|
|
||||||
///
|
|
||||||
/// The entry in the legend for this series will be returned to its normal
|
|
||||||
/// color if it was previously hidden.
|
|
||||||
@protected
|
|
||||||
void showSeries(String seriesId) {
|
|
||||||
_hiddenSeriesList.removeWhere((id) => id == seriesId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether or not a given series [seriesId] is currently hidden.
|
|
||||||
bool isSeriesHidden(String seriesId) {
|
|
||||||
return _hiddenSeriesList.contains(seriesId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,689 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:collection' show LinkedHashMap;
|
|
||||||
import 'dart:math' show max, min, Point, Rectangle;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
import '../../../common/color.dart' show Color;
|
|
||||||
import '../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../common/style/style_factory.dart' show StyleFactory;
|
|
||||||
import '../../../common/symbol_renderer.dart'
|
|
||||||
show CircleSymbolRenderer, SymbolRenderer;
|
|
||||||
import '../../cartesian/axis/axis.dart'
|
|
||||||
show ImmutableAxis, domainAxisKey, measureAxisKey;
|
|
||||||
import '../../cartesian/cartesian_chart.dart' show CartesianChart;
|
|
||||||
import '../../layout/layout_view.dart'
|
|
||||||
show
|
|
||||||
LayoutPosition,
|
|
||||||
LayoutView,
|
|
||||||
LayoutViewConfig,
|
|
||||||
LayoutViewPaintOrder,
|
|
||||||
ViewMeasuredSizes;
|
|
||||||
import '../base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
import '../chart_canvas.dart' show ChartCanvas, getAnimatedColor;
|
|
||||||
import '../datum_details.dart' show DatumDetails;
|
|
||||||
import '../processed_series.dart' show ImmutableSeries;
|
|
||||||
import '../selection_model/selection_model.dart'
|
|
||||||
show SelectionModel, SelectionModelType;
|
|
||||||
import 'chart_behavior.dart' show ChartBehavior;
|
|
||||||
|
|
||||||
/// Chart behavior that monitors the specified [SelectionModel] and renders a
|
|
||||||
/// dot for selected data.
|
|
||||||
///
|
|
||||||
/// Vertical or horizontal follow lines can optionally be drawn underneath the
|
|
||||||
/// rendered dots. Follow lines will be drawn in the combined area of the chart
|
|
||||||
/// draw area, and the draw area for any layout components that provide a
|
|
||||||
/// series draw area (e.g. [SymbolAnnotationRenderer]).
|
|
||||||
///
|
|
||||||
/// This is typically used for line charts to highlight segments.
|
|
||||||
///
|
|
||||||
/// It is used in combination with SelectNearest to update the selection model
|
|
||||||
/// and expand selection out to the domain value.
|
|
||||||
class LinePointHighlighter<D> implements ChartBehavior<D> {
|
|
||||||
final SelectionModelType selectionModelType;
|
|
||||||
|
|
||||||
/// Default radius of the dots if the series has no radius mapping function.
|
|
||||||
///
|
|
||||||
/// When no radius mapping function is provided, this value will be used as
|
|
||||||
/// is. [radiusPaddingPx] will not be added to [defaultRadiusPx].
|
|
||||||
final double defaultRadiusPx;
|
|
||||||
|
|
||||||
/// Additional radius value added to the radius of the selected data.
|
|
||||||
///
|
|
||||||
/// This value is only used when the series has a radius mapping function
|
|
||||||
/// defined.
|
|
||||||
final double radiusPaddingPx;
|
|
||||||
|
|
||||||
/// Whether or not to draw horizontal follow lines through the selected
|
|
||||||
/// points.
|
|
||||||
///
|
|
||||||
/// Defaults to drawing no horizontal follow lines.
|
|
||||||
final LinePointHighlighterFollowLineType showHorizontalFollowLine;
|
|
||||||
|
|
||||||
/// Whether or not to draw vertical follow lines through the selected points.
|
|
||||||
///
|
|
||||||
/// Defaults to drawing a vertical follow line only for the nearest datum.
|
|
||||||
final LinePointHighlighterFollowLineType showVerticalFollowLine;
|
|
||||||
|
|
||||||
/// The dash pattern to be used for drawing the line.
|
|
||||||
///
|
|
||||||
/// To disable dash pattern (to draw a solid line), pass in an empty list.
|
|
||||||
/// This is because if dashPattern is null or not set, it defaults to [1,3].
|
|
||||||
final List<int> dashPattern;
|
|
||||||
|
|
||||||
/// Whether or not follow lines should be drawn across the entire chart draw
|
|
||||||
/// area, or just from the axis to the point.
|
|
||||||
///
|
|
||||||
/// When disabled, measure follow lines will be drawn from the primary measure
|
|
||||||
/// axis to the point. In RTL mode, this means from the right-hand axis. In
|
|
||||||
/// LTR mode, from the left-hand axis.
|
|
||||||
final bool drawFollowLinesAcrossChart;
|
|
||||||
|
|
||||||
/// Renderer used to draw the highlighted points.
|
|
||||||
final SymbolRenderer symbolRenderer;
|
|
||||||
|
|
||||||
BaseChart<D> _chart;
|
|
||||||
|
|
||||||
_LinePointLayoutView _view;
|
|
||||||
|
|
||||||
LifecycleListener<D> _lifecycleListener;
|
|
||||||
|
|
||||||
/// Store a map of data drawn on the chart, mapped by series name.
|
|
||||||
///
|
|
||||||
/// [LinkedHashMap] is used to render the series on the canvas in the same
|
|
||||||
/// order as the data was provided by the selection model.
|
|
||||||
var _seriesPointMap = LinkedHashMap<String, _AnimatedPoint<D>>();
|
|
||||||
|
|
||||||
// Store a list of points that exist in the series data.
|
|
||||||
//
|
|
||||||
// This list will be used to remove any [_AnimatedPoint] that were rendered in
|
|
||||||
// previous draw cycles, but no longer have a corresponding datum in the new
|
|
||||||
// data.
|
|
||||||
final _currentKeys = <String>[];
|
|
||||||
|
|
||||||
LinePointHighlighter(
|
|
||||||
{SelectionModelType selectionModelType,
|
|
||||||
double defaultRadiusPx,
|
|
||||||
double radiusPaddingPx,
|
|
||||||
LinePointHighlighterFollowLineType showHorizontalFollowLine,
|
|
||||||
LinePointHighlighterFollowLineType showVerticalFollowLine,
|
|
||||||
List<int> dashPattern,
|
|
||||||
bool drawFollowLinesAcrossChart,
|
|
||||||
SymbolRenderer symbolRenderer})
|
|
||||||
: selectionModelType = selectionModelType ?? SelectionModelType.info,
|
|
||||||
defaultRadiusPx = defaultRadiusPx ?? 4.0,
|
|
||||||
radiusPaddingPx = radiusPaddingPx ?? 2.0,
|
|
||||||
showHorizontalFollowLine =
|
|
||||||
showHorizontalFollowLine ?? LinePointHighlighterFollowLineType.none,
|
|
||||||
showVerticalFollowLine = showVerticalFollowLine ??
|
|
||||||
LinePointHighlighterFollowLineType.nearest,
|
|
||||||
dashPattern = dashPattern ?? [1, 3],
|
|
||||||
drawFollowLinesAcrossChart = drawFollowLinesAcrossChart ?? true,
|
|
||||||
symbolRenderer = symbolRenderer ?? CircleSymbolRenderer() {
|
|
||||||
_lifecycleListener =
|
|
||||||
LifecycleListener<D>(onAxisConfigured: _updateViewData);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
_chart = chart;
|
|
||||||
|
|
||||||
_view = _LinePointLayoutView<D>(
|
|
||||||
chart: chart,
|
|
||||||
layoutPaintOrder: LayoutViewPaintOrder.linePointHighlighter,
|
|
||||||
showHorizontalFollowLine: showHorizontalFollowLine,
|
|
||||||
showVerticalFollowLine: showVerticalFollowLine,
|
|
||||||
dashPattern: dashPattern,
|
|
||||||
drawFollowLinesAcrossChart: drawFollowLinesAcrossChart,
|
|
||||||
symbolRenderer: symbolRenderer);
|
|
||||||
|
|
||||||
if (chart is CartesianChart) {
|
|
||||||
// Only vertical rendering is supported by this behavior.
|
|
||||||
assert((chart as CartesianChart).vertical);
|
|
||||||
}
|
|
||||||
|
|
||||||
chart.addView(_view);
|
|
||||||
|
|
||||||
chart.addLifecycleListener(_lifecycleListener);
|
|
||||||
chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.addSelectionChangedListener(_selectionChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart chart) {
|
|
||||||
chart.removeView(_view);
|
|
||||||
chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.removeSelectionChangedListener(_selectionChanged);
|
|
||||||
chart.removeLifecycleListener(_lifecycleListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _selectionChanged(SelectionModel selectionModel) {
|
|
||||||
_chart.redraw(skipLayout: true, skipAnimation: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateViewData() {
|
|
||||||
_currentKeys.clear();
|
|
||||||
|
|
||||||
final selectedDatumDetails =
|
|
||||||
_chart.getSelectedDatumDetails(selectionModelType);
|
|
||||||
|
|
||||||
// Create a new map each time to ensure that we have it sorted in the
|
|
||||||
// selection model order. This preserves the "nearestDetail" ordering, so
|
|
||||||
// that we render follow lines in the proper place.
|
|
||||||
final newSeriesMap = <String, _AnimatedPoint<D>>{};
|
|
||||||
|
|
||||||
for (DatumDetails<D> detail in selectedDatumDetails) {
|
|
||||||
if (detail == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final series = detail.series;
|
|
||||||
final datum = detail.datum;
|
|
||||||
|
|
||||||
final domainAxis = series.getAttr(domainAxisKey) as ImmutableAxis<D>;
|
|
||||||
final measureAxis = series.getAttr(measureAxisKey) as ImmutableAxis<num>;
|
|
||||||
|
|
||||||
final lineKey = series.id;
|
|
||||||
|
|
||||||
double radiusPx = (detail.radiusPx != null)
|
|
||||||
? detail.radiusPx.toDouble() + radiusPaddingPx
|
|
||||||
: defaultRadiusPx;
|
|
||||||
|
|
||||||
final pointKey = '$lineKey::${detail.domain}';
|
|
||||||
|
|
||||||
// If we already have a point for that key, use it.
|
|
||||||
_AnimatedPoint<D> animatingPoint;
|
|
||||||
if (_seriesPointMap.containsKey(pointKey)) {
|
|
||||||
animatingPoint = _seriesPointMap[pointKey];
|
|
||||||
} else {
|
|
||||||
// Create a new point and have it animate in from axis.
|
|
||||||
final point = _DatumPoint<D>(
|
|
||||||
datum: datum,
|
|
||||||
domain: detail.domain,
|
|
||||||
series: series,
|
|
||||||
x: domainAxis.getLocation(detail.domain),
|
|
||||||
y: measureAxis.getLocation(0.0));
|
|
||||||
|
|
||||||
animatingPoint = _AnimatedPoint<D>(
|
|
||||||
key: pointKey, overlaySeries: series.overlaySeries)
|
|
||||||
..setNewTarget(_PointRendererElement<D>()
|
|
||||||
..point = point
|
|
||||||
..color = detail.color
|
|
||||||
..fillColor = detail.fillColor
|
|
||||||
..radiusPx = radiusPx
|
|
||||||
..measureAxisPosition = measureAxis.getLocation(0.0)
|
|
||||||
..strokeWidthPx = detail.strokeWidthPx
|
|
||||||
..symbolRenderer = detail.symbolRenderer);
|
|
||||||
}
|
|
||||||
|
|
||||||
newSeriesMap[pointKey] = animatingPoint;
|
|
||||||
|
|
||||||
// Create a new line using the final point locations.
|
|
||||||
final point = _DatumPoint<D>(
|
|
||||||
datum: datum,
|
|
||||||
domain: detail.domain,
|
|
||||||
series: series,
|
|
||||||
x: detail.chartPosition.x,
|
|
||||||
y: detail.chartPosition.y);
|
|
||||||
|
|
||||||
// Update the set of points that still exist in the series data.
|
|
||||||
_currentKeys.add(pointKey);
|
|
||||||
|
|
||||||
// Get the point element we are going to setup.
|
|
||||||
final pointElement = _PointRendererElement<D>()
|
|
||||||
..point = point
|
|
||||||
..color = detail.color
|
|
||||||
..fillColor = detail.fillColor
|
|
||||||
..radiusPx = radiusPx
|
|
||||||
..measureAxisPosition = measureAxis.getLocation(0.0)
|
|
||||||
..strokeWidthPx = detail.strokeWidthPx
|
|
||||||
..symbolRenderer = detail.symbolRenderer;
|
|
||||||
|
|
||||||
animatingPoint.setNewTarget(pointElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate out points that don't exist anymore.
|
|
||||||
_seriesPointMap.forEach((key, point) {
|
|
||||||
if (_currentKeys.contains(point.key) != true) {
|
|
||||||
point.animateOut();
|
|
||||||
newSeriesMap[point.key] = point;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_seriesPointMap = newSeriesMap;
|
|
||||||
_view.seriesPointMap = _seriesPointMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'LinePointHighlighter-${selectionModelType.toString()}';
|
|
||||||
}
|
|
||||||
|
|
||||||
class _LinePointLayoutView<D> extends LayoutView {
|
|
||||||
final LayoutViewConfig layoutConfig;
|
|
||||||
|
|
||||||
final LinePointHighlighterFollowLineType showHorizontalFollowLine;
|
|
||||||
|
|
||||||
final LinePointHighlighterFollowLineType showVerticalFollowLine;
|
|
||||||
|
|
||||||
final BaseChart<D> chart;
|
|
||||||
|
|
||||||
final List<int> dashPattern;
|
|
||||||
|
|
||||||
Rectangle<int> _drawAreaBounds;
|
|
||||||
|
|
||||||
Rectangle<int> get drawBounds => _drawAreaBounds;
|
|
||||||
|
|
||||||
final bool drawFollowLinesAcrossChart;
|
|
||||||
|
|
||||||
final SymbolRenderer symbolRenderer;
|
|
||||||
|
|
||||||
GraphicsFactory graphicsFactory;
|
|
||||||
|
|
||||||
/// Store a map of series drawn on the chart, mapped by series name.
|
|
||||||
///
|
|
||||||
/// [LinkedHashMap] is used to render the series on the canvas in the same
|
|
||||||
/// order as the data was given to the chart.
|
|
||||||
LinkedHashMap<String, _AnimatedPoint<D>> _seriesPointMap;
|
|
||||||
|
|
||||||
_LinePointLayoutView({
|
|
||||||
@required this.chart,
|
|
||||||
@required int layoutPaintOrder,
|
|
||||||
@required this.showHorizontalFollowLine,
|
|
||||||
@required this.showVerticalFollowLine,
|
|
||||||
@required this.symbolRenderer,
|
|
||||||
this.dashPattern,
|
|
||||||
this.drawFollowLinesAcrossChart,
|
|
||||||
}) : this.layoutConfig = LayoutViewConfig(
|
|
||||||
paintOrder: LayoutViewPaintOrder.linePointHighlighter,
|
|
||||||
position: LayoutPosition.DrawArea,
|
|
||||||
positionOrder: layoutPaintOrder);
|
|
||||||
|
|
||||||
set seriesPointMap(LinkedHashMap<String, _AnimatedPoint<D>> value) {
|
|
||||||
_seriesPointMap = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
|
|
||||||
this._drawAreaBounds = drawAreaBounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(ChartCanvas canvas, double animationPercent) {
|
|
||||||
if (_seriesPointMap == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up the lines that no longer exist.
|
|
||||||
if (animationPercent == 1.0) {
|
|
||||||
final keysToRemove = <String>[];
|
|
||||||
|
|
||||||
_seriesPointMap.forEach((key, point) {
|
|
||||||
if (point.animatingOut) {
|
|
||||||
keysToRemove.add(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
keysToRemove.forEach((key) => _seriesPointMap.remove(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
final points = <_PointRendererElement<D>>[];
|
|
||||||
_seriesPointMap.forEach((key, point) {
|
|
||||||
points.add(point.getCurrentPoint(animationPercent));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build maps of the position where the follow lines should stop for each
|
|
||||||
// selected data point.
|
|
||||||
final endPointPerValueVertical = <int, int>{};
|
|
||||||
final endPointPerValueHorizontal = <int, int>{};
|
|
||||||
|
|
||||||
for (_PointRendererElement<D> pointElement in points) {
|
|
||||||
if (pointElement.point.x == null || pointElement.point.y == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final roundedX = pointElement.point.x.round();
|
|
||||||
final roundedY = pointElement.point.y.round();
|
|
||||||
|
|
||||||
// Get the Y value closest to the top of the chart for this X position.
|
|
||||||
if (endPointPerValueVertical[roundedX] == null) {
|
|
||||||
endPointPerValueVertical[roundedX] = roundedY;
|
|
||||||
} else {
|
|
||||||
// In the nearest case, we rely on the selected data always starting
|
|
||||||
// with the nearest point. In this case, we don't care about the rest of
|
|
||||||
// the selected data positions.
|
|
||||||
if (showVerticalFollowLine !=
|
|
||||||
LinePointHighlighterFollowLineType.nearest) {
|
|
||||||
endPointPerValueVertical[roundedX] =
|
|
||||||
min(endPointPerValueVertical[roundedX], roundedY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the X value closest to the "end" side of the chart for this Y
|
|
||||||
// position.
|
|
||||||
if (endPointPerValueHorizontal[roundedY] == null) {
|
|
||||||
endPointPerValueHorizontal[roundedY] = roundedX;
|
|
||||||
} else {
|
|
||||||
// In the nearest case, we rely on the selected data always starting
|
|
||||||
// with the nearest point. In this case, we don't care about the rest of
|
|
||||||
// the selected data positions.
|
|
||||||
if (showHorizontalFollowLine !=
|
|
||||||
LinePointHighlighterFollowLineType.nearest) {
|
|
||||||
endPointPerValueHorizontal[roundedY] =
|
|
||||||
max(endPointPerValueHorizontal[roundedY], roundedX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var shouldShowHorizontalFollowLine = showHorizontalFollowLine ==
|
|
||||||
LinePointHighlighterFollowLineType.all ||
|
|
||||||
showHorizontalFollowLine == LinePointHighlighterFollowLineType.nearest;
|
|
||||||
|
|
||||||
var shouldShowVerticalFollowLine = showVerticalFollowLine ==
|
|
||||||
LinePointHighlighterFollowLineType.all ||
|
|
||||||
showVerticalFollowLine == LinePointHighlighterFollowLineType.nearest;
|
|
||||||
|
|
||||||
// Keep track of points for which we've already drawn lines.
|
|
||||||
final paintedHorizontalLinePositions = <num>[];
|
|
||||||
final paintedVerticalLinePositions = <num>[];
|
|
||||||
|
|
||||||
final drawBounds = chart.drawableLayoutAreaBounds;
|
|
||||||
|
|
||||||
final rtl = chart.context.isRtl;
|
|
||||||
|
|
||||||
// Draw the follow lines first, below all of the highlight shapes.
|
|
||||||
for (_PointRendererElement<D> pointElement in points) {
|
|
||||||
if (pointElement.point.x == null || pointElement.point.y == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final roundedX = pointElement.point.x.round();
|
|
||||||
final roundedY = pointElement.point.y.round();
|
|
||||||
|
|
||||||
// Draw the horizontal follow line.
|
|
||||||
if (shouldShowHorizontalFollowLine &&
|
|
||||||
!paintedHorizontalLinePositions.contains(roundedY)) {
|
|
||||||
int leftBound;
|
|
||||||
int rightBound;
|
|
||||||
|
|
||||||
if (drawFollowLinesAcrossChart) {
|
|
||||||
// RTL and LTR both go across the whole draw area.
|
|
||||||
leftBound = drawBounds.left;
|
|
||||||
rightBound = drawBounds.left + drawBounds.width;
|
|
||||||
} else {
|
|
||||||
final x = endPointPerValueHorizontal[roundedY];
|
|
||||||
|
|
||||||
// RTL goes from the point to the right edge. LTR goes from the left
|
|
||||||
// edge to the point.
|
|
||||||
leftBound = rtl ? x : drawBounds.left;
|
|
||||||
rightBound = rtl ? drawBounds.left + drawBounds.width : x;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.drawLine(
|
|
||||||
points: [
|
|
||||||
Point<num>(leftBound, pointElement.point.y),
|
|
||||||
Point<num>(rightBound, pointElement.point.y),
|
|
||||||
],
|
|
||||||
stroke: StyleFactory.style.linePointHighlighterColor,
|
|
||||||
strokeWidthPx: 1.0,
|
|
||||||
dashPattern: [1, 3]);
|
|
||||||
|
|
||||||
if (showHorizontalFollowLine ==
|
|
||||||
LinePointHighlighterFollowLineType.nearest) {
|
|
||||||
shouldShowHorizontalFollowLine = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
paintedHorizontalLinePositions.add(roundedY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the vertical follow line.
|
|
||||||
if (shouldShowVerticalFollowLine &&
|
|
||||||
!paintedVerticalLinePositions.contains(roundedX)) {
|
|
||||||
final topBound = drawFollowLinesAcrossChart
|
|
||||||
? drawBounds.top
|
|
||||||
: endPointPerValueVertical[roundedX];
|
|
||||||
|
|
||||||
canvas.drawLine(
|
|
||||||
points: [
|
|
||||||
Point<num>(pointElement.point.x, topBound),
|
|
||||||
Point<num>(
|
|
||||||
pointElement.point.x, drawBounds.top + drawBounds.height),
|
|
||||||
],
|
|
||||||
stroke: StyleFactory.style.linePointHighlighterColor,
|
|
||||||
strokeWidthPx: 1.0,
|
|
||||||
dashPattern: dashPattern);
|
|
||||||
|
|
||||||
if (showVerticalFollowLine ==
|
|
||||||
LinePointHighlighterFollowLineType.nearest) {
|
|
||||||
shouldShowVerticalFollowLine = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
paintedVerticalLinePositions.add(roundedX);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldShowHorizontalFollowLine && !shouldShowVerticalFollowLine) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the highlight shapes on top of all follow lines.
|
|
||||||
for (_PointRendererElement<D> pointElement in points) {
|
|
||||||
if (pointElement.point.x == null || pointElement.point.y == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
final bounds = Rectangle<double>(
|
|
||||||
pointElement.point.x - pointElement.radiusPx,
|
|
||||||
pointElement.point.y - pointElement.radiusPx,
|
|
||||||
pointElement.radiusPx * 2,
|
|
||||||
pointElement.radiusPx * 2);
|
|
||||||
|
|
||||||
// Draw the highlight dot. Use the [SymbolRenderer] from the datum if one
|
|
||||||
// is defined.
|
|
||||||
(pointElement.symbolRenderer ?? symbolRenderer).paint(canvas, bounds,
|
|
||||||
fillColor: pointElement.fillColor,
|
|
||||||
strokeColor: pointElement.color,
|
|
||||||
strokeWidthPx: pointElement.strokeWidthPx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Rectangle<int> get componentBounds => this._drawAreaBounds;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isSeriesRenderer => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DatumPoint<D> extends Point<double> {
|
|
||||||
final dynamic datum;
|
|
||||||
final D domain;
|
|
||||||
final ImmutableSeries<D> series;
|
|
||||||
|
|
||||||
_DatumPoint({this.datum, this.domain, this.series, double x, double y})
|
|
||||||
: super(x, y);
|
|
||||||
|
|
||||||
factory _DatumPoint.from(_DatumPoint<D> other, [double x, double y]) {
|
|
||||||
return _DatumPoint<D>(
|
|
||||||
datum: other.datum,
|
|
||||||
domain: other.domain,
|
|
||||||
series: other.series,
|
|
||||||
x: x ?? other.x,
|
|
||||||
y: y ?? other.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PointRendererElement<D> {
|
|
||||||
_DatumPoint<D> point;
|
|
||||||
Color color;
|
|
||||||
Color fillColor;
|
|
||||||
double radiusPx;
|
|
||||||
double measureAxisPosition;
|
|
||||||
double strokeWidthPx;
|
|
||||||
SymbolRenderer symbolRenderer;
|
|
||||||
|
|
||||||
_PointRendererElement<D> clone() {
|
|
||||||
return _PointRendererElement<D>()
|
|
||||||
..point = this.point
|
|
||||||
..color = this.color
|
|
||||||
..fillColor = this.fillColor
|
|
||||||
..measureAxisPosition = this.measureAxisPosition
|
|
||||||
..radiusPx = this.radiusPx
|
|
||||||
..strokeWidthPx = this.strokeWidthPx
|
|
||||||
..symbolRenderer = this.symbolRenderer;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateAnimationPercent(_PointRendererElement previous,
|
|
||||||
_PointRendererElement target, double animationPercent) {
|
|
||||||
final targetPoint = target.point;
|
|
||||||
final previousPoint = previous.point;
|
|
||||||
|
|
||||||
final x = _lerpDouble(previousPoint.x, targetPoint.x, animationPercent);
|
|
||||||
|
|
||||||
final y = _lerpDouble(previousPoint.y, targetPoint.y, animationPercent);
|
|
||||||
|
|
||||||
point = _DatumPoint<D>.from(targetPoint, x, y);
|
|
||||||
|
|
||||||
color = getAnimatedColor(previous.color, target.color, animationPercent);
|
|
||||||
|
|
||||||
fillColor = getAnimatedColor(
|
|
||||||
previous.fillColor, target.fillColor, animationPercent);
|
|
||||||
|
|
||||||
radiusPx =
|
|
||||||
_lerpDouble(previous.radiusPx, target.radiusPx, animationPercent);
|
|
||||||
|
|
||||||
if (target.strokeWidthPx != null && previous.strokeWidthPx != null) {
|
|
||||||
strokeWidthPx = (((target.strokeWidthPx - previous.strokeWidthPx) *
|
|
||||||
animationPercent) +
|
|
||||||
previous.strokeWidthPx);
|
|
||||||
} else {
|
|
||||||
strokeWidthPx = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Linear interpolation for doubles.
|
|
||||||
///
|
|
||||||
/// If either [a] or [b] is null, return null.
|
|
||||||
/// This is different than Flutter's lerpDouble method, we want to return null
|
|
||||||
/// instead of assuming it is 0.0.
|
|
||||||
double _lerpDouble(double a, double b, double t) {
|
|
||||||
if (a == null || b == null) return null;
|
|
||||||
return a + (b - a) * t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedPoint<D> {
|
|
||||||
final String key;
|
|
||||||
final bool overlaySeries;
|
|
||||||
|
|
||||||
_PointRendererElement<D> _previousPoint;
|
|
||||||
_PointRendererElement<D> _targetPoint;
|
|
||||||
_PointRendererElement<D> _currentPoint;
|
|
||||||
|
|
||||||
// Flag indicating whether this point is being animated out of the chart.
|
|
||||||
bool animatingOut = false;
|
|
||||||
|
|
||||||
_AnimatedPoint({@required this.key, @required this.overlaySeries});
|
|
||||||
|
|
||||||
/// Animates a point that was removed from the series out of the view.
|
|
||||||
///
|
|
||||||
/// This should be called in place of "setNewTarget" for points that represent
|
|
||||||
/// data that has been removed from the series.
|
|
||||||
///
|
|
||||||
/// Animates the height of the point down to the measure axis position
|
|
||||||
/// (position of 0).
|
|
||||||
void animateOut() {
|
|
||||||
final newTarget = _currentPoint.clone();
|
|
||||||
|
|
||||||
// Set the target measure value to the axis position for all points.
|
|
||||||
final targetPoint = newTarget.point;
|
|
||||||
|
|
||||||
final newPoint = _DatumPoint<D>.from(targetPoint, targetPoint.x,
|
|
||||||
newTarget.measureAxisPosition.roundToDouble());
|
|
||||||
|
|
||||||
newTarget.point = newPoint;
|
|
||||||
|
|
||||||
// Animate the radius to 0 so that we don't get a lingering point after
|
|
||||||
// animation is done.
|
|
||||||
newTarget.radiusPx = 0.0;
|
|
||||||
|
|
||||||
setNewTarget(newTarget);
|
|
||||||
animatingOut = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setNewTarget(_PointRendererElement<D> newTarget) {
|
|
||||||
animatingOut = false;
|
|
||||||
_currentPoint ??= newTarget.clone();
|
|
||||||
_previousPoint = _currentPoint.clone();
|
|
||||||
_targetPoint = newTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
_PointRendererElement<D> getCurrentPoint(double animationPercent) {
|
|
||||||
if (animationPercent == 1.0 || _previousPoint == null) {
|
|
||||||
_currentPoint = _targetPoint;
|
|
||||||
_previousPoint = _targetPoint;
|
|
||||||
return _currentPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentPoint.updateAnimationPercent(
|
|
||||||
_previousPoint, _targetPoint, animationPercent);
|
|
||||||
|
|
||||||
return _currentPoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type of follow line(s) to draw.
|
|
||||||
enum LinePointHighlighterFollowLineType {
|
|
||||||
/// Draw a follow line for only the nearest point in the selection.
|
|
||||||
nearest,
|
|
||||||
|
|
||||||
/// Draw no follow lines.
|
|
||||||
none,
|
|
||||||
|
|
||||||
/// Draw a follow line for every point in the selection.
|
|
||||||
all,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper class that exposes fewer private internal properties for unit tests.
|
|
||||||
@visibleForTesting
|
|
||||||
class LinePointHighlighterTester<D> {
|
|
||||||
final LinePointHighlighter<D> behavior;
|
|
||||||
|
|
||||||
LinePointHighlighterTester(this.behavior);
|
|
||||||
|
|
||||||
int getSelectionLength() {
|
|
||||||
return behavior._seriesPointMap.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isDatumSelected(D datum) {
|
|
||||||
var contains = false;
|
|
||||||
|
|
||||||
behavior._seriesPointMap.forEach((key, point) {
|
|
||||||
if (point._currentPoint.point.datum == datum) {
|
|
||||||
contains = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return contains;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,126 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import '../../../../common/gesture_listener.dart' show GestureListener;
|
|
||||||
import '../../base_chart.dart' show BaseChart;
|
|
||||||
import '../../behavior/chart_behavior.dart' show ChartBehavior;
|
|
||||||
import '../../selection_model/selection_model.dart' show SelectionModelType;
|
|
||||||
import 'selection_trigger.dart' show SelectionTrigger;
|
|
||||||
|
|
||||||
/// Chart behavior that listens to tap event trigges and locks the specified
|
|
||||||
/// [SelectionModel]. This is used to prevent further updates to the selection
|
|
||||||
/// model, until it is unlocked again.
|
|
||||||
///
|
|
||||||
/// SelectionModels that can be updated:
|
|
||||||
/// info - To view the details of the selected items (ie: hover for web).
|
|
||||||
/// action - To select an item as an input, drill, or other selection.
|
|
||||||
///
|
|
||||||
/// You can add one LockSelection for each model type that you are updating.
|
|
||||||
/// Any previous LockSelection behavior for that selection model will be
|
|
||||||
/// removed.
|
|
||||||
class LockSelection<D> implements ChartBehavior<D> {
|
|
||||||
GestureListener _listener;
|
|
||||||
|
|
||||||
/// Type of selection model that should be updated by input events.
|
|
||||||
final SelectionModelType selectionModelType;
|
|
||||||
|
|
||||||
/// Type of input event that should trigger selection.
|
|
||||||
final SelectionTrigger eventTrigger = SelectionTrigger.tap;
|
|
||||||
|
|
||||||
BaseChart<D> _chart;
|
|
||||||
|
|
||||||
LockSelection({this.selectionModelType = SelectionModelType.info}) {
|
|
||||||
// Setup the appropriate gesture listening.
|
|
||||||
switch (this.eventTrigger) {
|
|
||||||
case SelectionTrigger.tap:
|
|
||||||
_listener = GestureListener(onTapTest: _onTapTest, onTap: _onSelect);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw ArgumentError('LockSelection does not support the event '
|
|
||||||
'trigger "${this.eventTrigger}"');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onTapTest(Point<double> chartPoint) {
|
|
||||||
// If the tap is within the drawArea, then claim the event from others.
|
|
||||||
return _chart.pointWithinRenderer(chartPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onSelect(Point<double> chartPoint, [double ignored]) {
|
|
||||||
// Skip events that occur outside the drawArea for any series renderer.
|
|
||||||
if (!_chart.pointWithinRenderer(chartPoint)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final selectionModel = _chart.getSelectionModel(selectionModelType);
|
|
||||||
|
|
||||||
// Do nothing if the chart has no selection model.
|
|
||||||
if (selectionModel == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not lock the selection model if there is no selection. Locking nothing
|
|
||||||
// would result in a very confusing user interface as the user tries to
|
|
||||||
// interact with content on the chart.
|
|
||||||
if (!selectionModel.locked && !selectionModel.hasAnySelection) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle the lock state.
|
|
||||||
selectionModel.locked = !selectionModel.locked;
|
|
||||||
|
|
||||||
// If the model was just unlocked, clear the selection to dismiss any stale
|
|
||||||
// behavior elements. A new hovercard/etc. will appear after the user
|
|
||||||
// triggers a new gesture.
|
|
||||||
if (!selectionModel.locked) {
|
|
||||||
selectionModel.clearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
_chart = chart;
|
|
||||||
chart.addGestureListener(_listener);
|
|
||||||
|
|
||||||
// TODO: Update this dynamically based on tappable location.
|
|
||||||
switch (this.eventTrigger) {
|
|
||||||
case SelectionTrigger.tap:
|
|
||||||
case SelectionTrigger.tapAndDrag:
|
|
||||||
case SelectionTrigger.pressHold:
|
|
||||||
case SelectionTrigger.longPressHold:
|
|
||||||
chart.registerTappable(this);
|
|
||||||
break;
|
|
||||||
case SelectionTrigger.hover:
|
|
||||||
default:
|
|
||||||
chart.unregisterTappable(this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart<D> chart) {
|
|
||||||
chart.removeGestureListener(_listener);
|
|
||||||
chart.unregisterTappable(this);
|
|
||||||
_chart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'LockSelection-${selectionModelType.toString()}}';
|
|
||||||
}
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import '../../../../common/gesture_listener.dart' show GestureListener;
|
|
||||||
import '../../base_chart.dart' show BaseChart;
|
|
||||||
import '../../behavior/chart_behavior.dart' show ChartBehavior;
|
|
||||||
import '../../datum_details.dart' show DatumDetails;
|
|
||||||
import '../../processed_series.dart' show ImmutableSeries;
|
|
||||||
import '../../selection_model/selection_model.dart' show SelectionModelType;
|
|
||||||
import '../../series_datum.dart' show SeriesDatum;
|
|
||||||
import 'selection_trigger.dart' show SelectionTrigger;
|
|
||||||
|
|
||||||
/// Chart behavior that listens to the given eventTrigger and updates the
|
|
||||||
/// specified [SelectionModel]. This is used to pair input events to behaviors
|
|
||||||
/// that listen to selection changes.
|
|
||||||
///
|
|
||||||
/// Input event types:
|
|
||||||
/// hover (default) - Mouse over/near data.
|
|
||||||
/// tap - Mouse/Touch on/near data.
|
|
||||||
/// pressHold - Mouse/Touch and drag across the data instead of panning.
|
|
||||||
/// longPressHold - Mouse/Touch for a while in one place then drag across the
|
|
||||||
/// data.
|
|
||||||
///
|
|
||||||
/// SelectionModels that can be updated:
|
|
||||||
/// info - To view the details of the selected items (ie: hover for web).
|
|
||||||
/// action - To select an item as an input, drill, or other selection.
|
|
||||||
///
|
|
||||||
/// Other options available
|
|
||||||
/// [expandToDomain] - All data points that match the domain value of the
|
|
||||||
/// closest data point from each Series will be included in the selection.
|
|
||||||
/// The selection is limited to the hovered component area unless
|
|
||||||
/// [selectAcrossAllSeriesRendererComponents] is set to true. (Default:
|
|
||||||
/// true)
|
|
||||||
/// [selectAcrossAllSeriesRendererComponents] - Events in any component that
|
|
||||||
/// draw Series data will propagate to other components that draw Series
|
|
||||||
/// data to get a union of points that match across all series renderer
|
|
||||||
/// components. This is useful when components in the margins draw series
|
|
||||||
/// data and a selection is supposed to bridge the two adjacent
|
|
||||||
/// components. (Default: true)
|
|
||||||
/// [selectClosestSeries] - If true, the closest Series itself will be marked
|
|
||||||
/// as selected in addition to the datum. This is useful for features like
|
|
||||||
/// highlighting the closest Series. (Default: true)
|
|
||||||
///
|
|
||||||
/// You can add one SelectNearest for each model type that you are updating.
|
|
||||||
/// Any previous SelectNearest behavior for that selection model will be
|
|
||||||
/// removed.
|
|
||||||
class SelectNearest<D> implements ChartBehavior<D> {
|
|
||||||
GestureListener _listener;
|
|
||||||
|
|
||||||
/// Type of selection model that should be updated by input events.
|
|
||||||
final SelectionModelType selectionModelType;
|
|
||||||
|
|
||||||
/// Type of input event that should trigger selection.
|
|
||||||
final SelectionTrigger eventTrigger;
|
|
||||||
|
|
||||||
/// Whether or not all data points that match the domain value of the closest
|
|
||||||
/// data point from each Series will be included in the selection.
|
|
||||||
///
|
|
||||||
/// The selection is limited to the hovered component area unless
|
|
||||||
/// [selectAcrossAllSeriesRendererComponents] is set to true.
|
|
||||||
final bool expandToDomain;
|
|
||||||
|
|
||||||
/// Whether or not events in any component that draw Series data will
|
|
||||||
/// propagate to other components that draw Series data to get a union of
|
|
||||||
/// points that match across all series renderer components.
|
|
||||||
///
|
|
||||||
/// This is useful when components in the margins draw series data and a
|
|
||||||
/// selection is supposed to bridge the two adjacent components.
|
|
||||||
final bool selectAcrossAllSeriesRendererComponents;
|
|
||||||
|
|
||||||
/// Whether or not the closest Series itself will be marked as selected in
|
|
||||||
/// addition to the datum.
|
|
||||||
final bool selectClosestSeries;
|
|
||||||
|
|
||||||
/// The farthest away a domain value can be from the mouse position on the
|
|
||||||
/// domain axis before we'll ignore the datum.
|
|
||||||
///
|
|
||||||
/// This allows sparse data to not get selected until the mouse is some
|
|
||||||
/// reasonable distance. Defaults to no maximum distance.
|
|
||||||
final int maximumDomainDistancePx;
|
|
||||||
|
|
||||||
BaseChart<D> _chart;
|
|
||||||
|
|
||||||
bool _delaySelect = false;
|
|
||||||
|
|
||||||
SelectNearest(
|
|
||||||
{this.selectionModelType = SelectionModelType.info,
|
|
||||||
this.expandToDomain = true,
|
|
||||||
this.selectAcrossAllSeriesRendererComponents = true,
|
|
||||||
this.selectClosestSeries = true,
|
|
||||||
this.eventTrigger = SelectionTrigger.hover,
|
|
||||||
this.maximumDomainDistancePx}) {
|
|
||||||
// Setup the appropriate gesture listening.
|
|
||||||
switch (this.eventTrigger) {
|
|
||||||
case SelectionTrigger.tap:
|
|
||||||
_listener = GestureListener(onTapTest: _onTapTest, onTap: _onSelect);
|
|
||||||
break;
|
|
||||||
case SelectionTrigger.tapAndDrag:
|
|
||||||
_listener = GestureListener(
|
|
||||||
onTapTest: _onTapTest,
|
|
||||||
onTap: _onSelect,
|
|
||||||
onDragStart: _onSelect,
|
|
||||||
onDragUpdate: _onSelect,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case SelectionTrigger.pressHold:
|
|
||||||
_listener = GestureListener(
|
|
||||||
onTapTest: _onTapTest,
|
|
||||||
onLongPress: _onSelect,
|
|
||||||
onDragStart: _onSelect,
|
|
||||||
onDragUpdate: _onSelect,
|
|
||||||
onDragEnd: _onDeselectAll);
|
|
||||||
break;
|
|
||||||
case SelectionTrigger.longPressHold:
|
|
||||||
_listener = GestureListener(
|
|
||||||
onTapTest: _onTapTest,
|
|
||||||
onLongPress: _onLongPressSelect,
|
|
||||||
onDragStart: _onSelect,
|
|
||||||
onDragUpdate: _onSelect,
|
|
||||||
onDragEnd: _onDeselectAll);
|
|
||||||
break;
|
|
||||||
case SelectionTrigger.hover:
|
|
||||||
default:
|
|
||||||
_listener = GestureListener(onHover: _onSelect);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onTapTest(Point<double> chartPoint) {
|
|
||||||
// If the tap is within the drawArea, then claim the event from others.
|
|
||||||
_delaySelect = eventTrigger == SelectionTrigger.longPressHold;
|
|
||||||
return _chart.pointWithinRenderer(chartPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onLongPressSelect(Point<double> chartPoint) {
|
|
||||||
_delaySelect = false;
|
|
||||||
return _onSelect(chartPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onSelect(Point<double> chartPoint, [double ignored]) {
|
|
||||||
// If the selection is delayed (waiting for long press), then quit early.
|
|
||||||
if (_delaySelect) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var details = _chart.getNearestDatumDetailPerSeries(
|
|
||||||
chartPoint, selectAcrossAllSeriesRendererComponents);
|
|
||||||
|
|
||||||
final seriesList = <ImmutableSeries<D>>[];
|
|
||||||
var seriesDatumList = <SeriesDatum<D>>[];
|
|
||||||
|
|
||||||
if (details != null && details.isNotEmpty) {
|
|
||||||
details.sort((a, b) => a.domainDistance.compareTo(b.domainDistance));
|
|
||||||
|
|
||||||
if (maximumDomainDistancePx == null ||
|
|
||||||
details[0].domainDistance <= maximumDomainDistancePx) {
|
|
||||||
seriesDatumList = expandToDomain
|
|
||||||
? _expandToDomain(details.first)
|
|
||||||
: [SeriesDatum<D>(details.first.series, details.first.datum)];
|
|
||||||
|
|
||||||
// Filter out points from overlay series.
|
|
||||||
seriesDatumList.removeWhere((datum) => datum.series.overlaySeries);
|
|
||||||
|
|
||||||
if (selectClosestSeries && seriesList.isEmpty) {
|
|
||||||
if (details.first.series.overlaySeries) {
|
|
||||||
// If the closest "details" was from an overlay series, grab the
|
|
||||||
// closest remaining series instead. In this case, we need to sort a
|
|
||||||
// copy of the list by domain distance because we do not want to
|
|
||||||
// re-order the actual return values here.
|
|
||||||
final sortedSeriesDatumList =
|
|
||||||
List<SeriesDatum<D>>.from(seriesDatumList);
|
|
||||||
sortedSeriesDatumList.sort((a, b) =>
|
|
||||||
a.datum.domainDistance.compareTo(b.datum.domainDistance));
|
|
||||||
seriesList.add(sortedSeriesDatumList.first.series);
|
|
||||||
} else {
|
|
||||||
seriesList.add(details.first.series);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return _chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.updateSelection(seriesDatumList, seriesList);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onDeselectAll(_, __, ___) {
|
|
||||||
// If the selection is delayed (waiting for long press), then quit early.
|
|
||||||
if (_delaySelect) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.updateSelection(<SeriesDatum<D>>[], <ImmutableSeries<D>>[]);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<SeriesDatum<D>> _expandToDomain(DatumDetails<D> nearestDetails) {
|
|
||||||
// Make sure that the "nearest" datum is at the top of the list.
|
|
||||||
final data = <SeriesDatum<D>>[
|
|
||||||
SeriesDatum(nearestDetails.series, nearestDetails.datum)
|
|
||||||
];
|
|
||||||
final nearestDomain = nearestDetails.domain;
|
|
||||||
|
|
||||||
for (ImmutableSeries<D> series in _chart.currentSeriesList) {
|
|
||||||
final domainFn = series.domainFn;
|
|
||||||
final domainLowerBoundFn = series.domainLowerBoundFn;
|
|
||||||
final domainUpperBoundFn = series.domainUpperBoundFn;
|
|
||||||
final testBounds =
|
|
||||||
domainLowerBoundFn != null && domainUpperBoundFn != null;
|
|
||||||
|
|
||||||
for (var i = 0; i < series.data.length; i++) {
|
|
||||||
final datum = series.data[i];
|
|
||||||
final domain = domainFn(i);
|
|
||||||
|
|
||||||
// Don't re-add the nearest details.
|
|
||||||
if (nearestDetails.series == series && nearestDetails.datum == datum) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domain == nearestDomain) {
|
|
||||||
data.add(SeriesDatum(series, datum));
|
|
||||||
} else if (testBounds) {
|
|
||||||
final domainLowerBound = domainLowerBoundFn(i);
|
|
||||||
final domainUpperBound = domainUpperBoundFn(i);
|
|
||||||
|
|
||||||
var addDatum = false;
|
|
||||||
if (domainLowerBound != null && domainUpperBound != null) {
|
|
||||||
if (domain is int) {
|
|
||||||
addDatum = (domainLowerBound as int) <= (nearestDomain as int) &&
|
|
||||||
(nearestDomain as int) <= (domainUpperBound as int);
|
|
||||||
} else if (domain is double) {
|
|
||||||
addDatum =
|
|
||||||
(domainLowerBound as double) <= (nearestDomain as double) &&
|
|
||||||
(nearestDomain as double) <= (domainUpperBound as double);
|
|
||||||
} else if (domain is DateTime) {
|
|
||||||
addDatum = domainLowerBound == nearestDomain ||
|
|
||||||
domainUpperBound == nearestDomain ||
|
|
||||||
((domainLowerBound as DateTime)
|
|
||||||
.isBefore(nearestDomain as DateTime) &&
|
|
||||||
(nearestDomain as DateTime)
|
|
||||||
.isBefore(domainUpperBound as DateTime));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addDatum) {
|
|
||||||
data.add(SeriesDatum(series, datum));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
_chart = chart;
|
|
||||||
chart.addGestureListener(_listener);
|
|
||||||
|
|
||||||
// TODO: Update this dynamically based on tappable location.
|
|
||||||
switch (this.eventTrigger) {
|
|
||||||
case SelectionTrigger.tap:
|
|
||||||
case SelectionTrigger.tapAndDrag:
|
|
||||||
case SelectionTrigger.pressHold:
|
|
||||||
case SelectionTrigger.longPressHold:
|
|
||||||
chart.registerTappable(this);
|
|
||||||
break;
|
|
||||||
case SelectionTrigger.hover:
|
|
||||||
default:
|
|
||||||
chart.unregisterTappable(this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart<D> chart) {
|
|
||||||
chart.removeGestureListener(_listener);
|
|
||||||
chart.unregisterTappable(this);
|
|
||||||
_chart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'SelectNearest-${selectionModelType.toString()}}';
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
enum SelectionTrigger {
|
|
||||||
hover,
|
|
||||||
tap,
|
|
||||||
tapAndDrag,
|
|
||||||
pressHold,
|
|
||||||
longPressHold,
|
|
||||||
}
|
|
||||||
@@ -1,803 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
import '../../../../common/color.dart' show Color;
|
|
||||||
import '../../../../common/gesture_listener.dart' show GestureListener;
|
|
||||||
import '../../../../common/graphics_factory.dart' show GraphicsFactory;
|
|
||||||
import '../../../../common/math.dart' show clamp;
|
|
||||||
import '../../../../common/style/style_factory.dart' show StyleFactory;
|
|
||||||
import '../../../../common/symbol_renderer.dart'
|
|
||||||
show RectSymbolRenderer, SymbolRenderer;
|
|
||||||
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
|
|
||||||
import '../../../layout/layout_view.dart'
|
|
||||||
show
|
|
||||||
LayoutPosition,
|
|
||||||
LayoutView,
|
|
||||||
LayoutViewConfig,
|
|
||||||
LayoutViewPaintOrder,
|
|
||||||
LayoutViewPositionOrder,
|
|
||||||
ViewMeasuredSizes;
|
|
||||||
import '../../base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
import '../../behavior/chart_behavior.dart' show ChartBehavior;
|
|
||||||
import '../../chart_canvas.dart' show ChartCanvas, getAnimatedColor;
|
|
||||||
import '../selection/selection_trigger.dart' show SelectionTrigger;
|
|
||||||
|
|
||||||
/// Chart behavior that adds a slider widget to a chart. When the slider is
|
|
||||||
/// dropped after drag, it will report its domain position and nearest datum
|
|
||||||
/// value. This behavior only supports charts that use continuous scales.
|
|
||||||
///
|
|
||||||
/// Input event types:
|
|
||||||
/// tapAndDrag - Mouse/Touch on the handle and drag across the chart.
|
|
||||||
/// pressHold - Mouse/Touch on the handle and drag across the chart instead of
|
|
||||||
/// panning.
|
|
||||||
/// longPressHold - Mouse/Touch for a while on the handle, then drag across
|
|
||||||
/// the data.
|
|
||||||
class Slider<D> implements ChartBehavior<D> {
|
|
||||||
_SliderLayoutView _view;
|
|
||||||
|
|
||||||
GestureListener _gestureListener;
|
|
||||||
|
|
||||||
LifecycleListener<D> _lifecycleListener;
|
|
||||||
|
|
||||||
SliderEventListener<D> _sliderEventListener;
|
|
||||||
|
|
||||||
/// The order to paint slider on the canvas.
|
|
||||||
///
|
|
||||||
/// The smaller number is drawn first. This value should be relative to
|
|
||||||
/// LayoutPaintViewOrder.slider (e.g. LayoutViewPaintOrder.slider + 1).
|
|
||||||
int layoutPaintOrder;
|
|
||||||
|
|
||||||
/// Type of input event for the slider.
|
|
||||||
///
|
|
||||||
/// Input event types:
|
|
||||||
/// tapAndDrag - Mouse/Touch on the handle and drag across the chart.
|
|
||||||
/// pressHold - Mouse/Touch on the handle and drag across the chart instead
|
|
||||||
/// of panning.
|
|
||||||
/// longPressHold - Mouse/Touch for a while on the handle, then drag across
|
|
||||||
/// the data.
|
|
||||||
final SelectionTrigger eventTrigger;
|
|
||||||
|
|
||||||
/// Renderer for the handle. Defaults to a rectangle.
|
|
||||||
SymbolRenderer _handleRenderer;
|
|
||||||
|
|
||||||
/// Custom role ID for this slider
|
|
||||||
String _roleId;
|
|
||||||
|
|
||||||
/// Whether or not the slider will snap onto the nearest datum (by domain
|
|
||||||
/// distance) when dragged.
|
|
||||||
final bool snapToDatum;
|
|
||||||
|
|
||||||
/// Color and size styles for the slider.
|
|
||||||
SliderStyle _style;
|
|
||||||
|
|
||||||
CartesianChart<D> _chart;
|
|
||||||
|
|
||||||
/// Rendering data for the slider line and handle.
|
|
||||||
_AnimatedSlider _sliderHandle;
|
|
||||||
|
|
||||||
bool _delaySelect = false;
|
|
||||||
|
|
||||||
bool _handleDrag = false;
|
|
||||||
|
|
||||||
/// Current location of the slider line.
|
|
||||||
Point<int> _domainCenterPoint;
|
|
||||||
|
|
||||||
/// Previous location of the slider line.
|
|
||||||
///
|
|
||||||
/// This is used to track changes in the position of the slider caused by new
|
|
||||||
/// data being drawn on the chart.
|
|
||||||
Point<int> _previousDomainCenterPoint;
|
|
||||||
|
|
||||||
/// Bounding box for the slider drag handle.
|
|
||||||
Rectangle<int> _handleBounds;
|
|
||||||
|
|
||||||
/// Domain value of the current slider position.
|
|
||||||
///
|
|
||||||
/// This is saved in terms of domain instead of chart position so that we can
|
|
||||||
/// adjust the slider automatically when the chart is resized.
|
|
||||||
D _domainValue;
|
|
||||||
|
|
||||||
/// Event to fire during the chart's onPostrender event.
|
|
||||||
///
|
|
||||||
/// This should be set any time the state of the slider has changed.
|
|
||||||
SliderListenerDragState _dragStateToFireOnPostRender;
|
|
||||||
|
|
||||||
/// Constructs a [Slider].
|
|
||||||
///
|
|
||||||
/// [eventTrigger] sets the type of gesture handled by the slider.
|
|
||||||
///
|
|
||||||
/// [handleRenderer] draws a handle for the slider. Defaults to a rectangle.
|
|
||||||
///
|
|
||||||
/// [initialDomainValue] sets the initial position of the slider in domain
|
|
||||||
/// units. The default is the center of the chart.
|
|
||||||
///
|
|
||||||
/// [onChangeCallback] will be called when the position of the slider
|
|
||||||
/// changes during a drag event.
|
|
||||||
///
|
|
||||||
/// [roleId] optional custom role ID for the slider. This can be used to allow
|
|
||||||
/// multiple [Slider] behaviors on the same chart. Normally, there can only be
|
|
||||||
/// one slider (per event trigger type) on a chart. This setting allows for
|
|
||||||
/// configuring multiple independent sliders.
|
|
||||||
///
|
|
||||||
/// [snapToDatum] configures the slider to snap snap onto the nearest datum
|
|
||||||
/// (by domain distance) when dragged. By default, the slider can be
|
|
||||||
/// positioned anywhere along the domain axis.
|
|
||||||
///
|
|
||||||
/// [style] configures the color and sizing of the slider line and handle.
|
|
||||||
///
|
|
||||||
/// [layoutPaintOrder] configures the order in which the behavior should be
|
|
||||||
/// painted. This value should be relative to LayoutPaintViewOrder.slider.
|
|
||||||
/// (e.g. LayoutViewPaintOrder.slider + 1).
|
|
||||||
Slider(
|
|
||||||
{this.eventTrigger = SelectionTrigger.tapAndDrag,
|
|
||||||
SymbolRenderer handleRenderer,
|
|
||||||
D initialDomainValue,
|
|
||||||
SliderListenerCallback<D> onChangeCallback,
|
|
||||||
String roleId,
|
|
||||||
this.snapToDatum = false,
|
|
||||||
SliderStyle style,
|
|
||||||
this.layoutPaintOrder = LayoutViewPaintOrder.slider}) {
|
|
||||||
_handleRenderer = handleRenderer ?? RectSymbolRenderer();
|
|
||||||
_roleId = roleId ?? '';
|
|
||||||
_style = style ?? SliderStyle();
|
|
||||||
|
|
||||||
_domainValue = initialDomainValue;
|
|
||||||
if (_domainValue != null) {
|
|
||||||
_dragStateToFireOnPostRender = SliderListenerDragState.initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup the appropriate gesture listening.
|
|
||||||
switch (this.eventTrigger) {
|
|
||||||
case SelectionTrigger.tapAndDrag:
|
|
||||||
_gestureListener = GestureListener(
|
|
||||||
onTapTest: _onTapTest,
|
|
||||||
onTap: _onSelect,
|
|
||||||
onDragStart: _onSelect,
|
|
||||||
onDragUpdate: _onSelect,
|
|
||||||
onDragEnd: _onDragEnd);
|
|
||||||
break;
|
|
||||||
case SelectionTrigger.pressHold:
|
|
||||||
_gestureListener = GestureListener(
|
|
||||||
onTapTest: _onTapTest,
|
|
||||||
onLongPress: _onSelect,
|
|
||||||
onDragStart: _onSelect,
|
|
||||||
onDragUpdate: _onSelect,
|
|
||||||
onDragEnd: _onDragEnd);
|
|
||||||
break;
|
|
||||||
case SelectionTrigger.longPressHold:
|
|
||||||
_gestureListener = GestureListener(
|
|
||||||
onTapTest: _onTapTest,
|
|
||||||
onLongPress: _onLongPressSelect,
|
|
||||||
onDragStart: _onSelect,
|
|
||||||
onDragUpdate: _onSelect,
|
|
||||||
onDragEnd: _onDragEnd);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw ArgumentError('Slider does not support the event trigger '
|
|
||||||
'"${this.eventTrigger}"');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up chart draw cycle listeners.
|
|
||||||
_lifecycleListener = LifecycleListener<D>(
|
|
||||||
onData: _setInitialDragState,
|
|
||||||
onAxisConfigured: _updateViewData,
|
|
||||||
onPostrender: _fireChangeEvent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set up slider event listeners.
|
|
||||||
_sliderEventListener = SliderEventListener<D>(onChange: onChangeCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onTapTest(Point<double> chartPoint) {
|
|
||||||
_delaySelect = eventTrigger == SelectionTrigger.longPressHold;
|
|
||||||
_handleDrag = _sliderContainsPoint(chartPoint);
|
|
||||||
return _handleDrag;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onLongPressSelect(Point<double> chartPoint) {
|
|
||||||
_delaySelect = false;
|
|
||||||
return _onSelect(chartPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onSelect(Point<double> chartPoint, [double ignored]) {
|
|
||||||
// Skip events that occur outside the drawArea for any series renderer.
|
|
||||||
// If the selection is delayed (waiting for long press), then quit early.
|
|
||||||
if (!_handleDrag || _delaySelect) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the slider line along the domain axis, without adjusting the measure
|
|
||||||
// position.
|
|
||||||
final positionChanged = _moveSliderToPoint(chartPoint);
|
|
||||||
|
|
||||||
if (positionChanged) {
|
|
||||||
_dragStateToFireOnPostRender = SliderListenerDragState.drag;
|
|
||||||
|
|
||||||
_chart.redraw(skipAnimation: true, skipLayout: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _onDragEnd(Point<double> chartPoint, __, ___) {
|
|
||||||
// If the selection is delayed (waiting for long press), then quit early.
|
|
||||||
if (_delaySelect) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleDrag = false;
|
|
||||||
|
|
||||||
// If snapToDatum is enabled, use the x position of the nearest datum
|
|
||||||
// instead of the mouse point.
|
|
||||||
if (snapToDatum) {
|
|
||||||
final details = _chart.getNearestDatumDetailPerSeries(chartPoint, true);
|
|
||||||
if (details.isNotEmpty && details[0].chartPosition.x != null) {
|
|
||||||
// Only trigger an animating draw cycle if we need to move the slider.
|
|
||||||
if (_domainValue != details[0].domain) {
|
|
||||||
_moveSliderToDomain(details[0].domain);
|
|
||||||
|
|
||||||
// Always fire the end event to notify listeners that the gesture is
|
|
||||||
// over.
|
|
||||||
_dragStateToFireOnPostRender = SliderListenerDragState.end;
|
|
||||||
|
|
||||||
_chart.redraw(skipAnimation: false, skipLayout: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Move the slider line along the domain axis, without adjusting the
|
|
||||||
// measure position.
|
|
||||||
_moveSliderToPoint(chartPoint);
|
|
||||||
|
|
||||||
// Always fire the end event to notify listeners that the gesture is
|
|
||||||
// over.
|
|
||||||
_dragStateToFireOnPostRender = SliderListenerDragState.end;
|
|
||||||
|
|
||||||
_chart.redraw(skipAnimation: true, skipLayout: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _sliderContainsPoint(Point<double> chartPoint) {
|
|
||||||
return _handleBounds.containsPoint(chartPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the drag state to "initial" when new data is drawn on the chart.
|
|
||||||
void _setInitialDragState(_) {
|
|
||||||
_dragStateToFireOnPostRender = SliderListenerDragState.initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateViewData() {
|
|
||||||
_sliderHandle ??= _AnimatedSlider();
|
|
||||||
|
|
||||||
// If not set in the constructor, initial position for the handle is the
|
|
||||||
// center of the draw area.
|
|
||||||
_domainValue ??= _chart.domainAxis
|
|
||||||
.getDomain(_view.drawBounds.left + _view.drawBounds.width / 2)
|
|
||||||
.round();
|
|
||||||
|
|
||||||
// Possibly move the slider, if the axis values have changed since the last
|
|
||||||
// chart draw.
|
|
||||||
_moveSliderToDomain(_domainValue);
|
|
||||||
|
|
||||||
// Move the handle to the current event position.
|
|
||||||
final element = _SliderElement()
|
|
||||||
..domainCenterPoint =
|
|
||||||
Point<int>(_domainCenterPoint.x, _domainCenterPoint.y)
|
|
||||||
..buttonBounds = Rectangle<int>(_handleBounds.left, _handleBounds.top,
|
|
||||||
_handleBounds.width, _handleBounds.height)
|
|
||||||
..fill = _style.fillColor
|
|
||||||
..stroke = _style.strokeColor
|
|
||||||
..strokeWidthPx = _style.strokeWidthPx;
|
|
||||||
|
|
||||||
_sliderHandle.setNewTarget(element);
|
|
||||||
|
|
||||||
_view.sliderHandle = _sliderHandle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fires a [SliderListenerDragState] change event if needed.
|
|
||||||
void _fireChangeEvent(_) {
|
|
||||||
if (SliderListenerDragState == null ||
|
|
||||||
_sliderEventListener.onChange == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SliderListenerDragState dragState = _dragStateToFireOnPostRender;
|
|
||||||
|
|
||||||
// Initial drag state event should only be fired if the slider has moved
|
|
||||||
// since the last draw. We always set the initial drag state event when new
|
|
||||||
// data was drawn on the chart, since we might need to move the slider if
|
|
||||||
// the axis range changed.
|
|
||||||
if (dragState == SliderListenerDragState.initial &&
|
|
||||||
_previousDomainCenterPoint == _domainCenterPoint) {
|
|
||||||
dragState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset state.
|
|
||||||
_dragStateToFireOnPostRender = null;
|
|
||||||
_previousDomainCenterPoint = _domainCenterPoint;
|
|
||||||
|
|
||||||
// Bail out if the event was cancelled.
|
|
||||||
if (dragState == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire the event.
|
|
||||||
_sliderEventListener.onChange(
|
|
||||||
Point<int>(_domainCenterPoint.x, _domainCenterPoint.y),
|
|
||||||
_domainValue,
|
|
||||||
_roleId,
|
|
||||||
dragState);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the slider along the domain axis to [point].
|
|
||||||
///
|
|
||||||
/// If [point] exists beyond either edge of the draw area, it will be bound to
|
|
||||||
/// the nearest edge.
|
|
||||||
///
|
|
||||||
/// Updates [_domainValue] with the domain value located at [point]. For
|
|
||||||
/// ordinal axes, this might technically result in a domain value whose center
|
|
||||||
/// point lies slightly outside the draw area.
|
|
||||||
///
|
|
||||||
/// Updates [_domainCenterPoint] and [_handleBounds] with the new position of
|
|
||||||
/// the slider.
|
|
||||||
///
|
|
||||||
/// Returns whether or not the position actually changed. This will generally
|
|
||||||
/// be false if the mouse was dragged outside of the domain axis viewport.
|
|
||||||
bool _moveSliderToPoint(Point<double> point) {
|
|
||||||
var positionChanged = false;
|
|
||||||
|
|
||||||
if (_chart != null) {
|
|
||||||
final viewBounds = _view.componentBounds;
|
|
||||||
|
|
||||||
// Clamp the position to the edge of the viewport.
|
|
||||||
final position = clamp(point.x, viewBounds.left, viewBounds.right);
|
|
||||||
|
|
||||||
positionChanged = (_previousDomainCenterPoint != null &&
|
|
||||||
position != _previousDomainCenterPoint.x);
|
|
||||||
|
|
||||||
// Reset the domain value if the position was outside of the chart.
|
|
||||||
_domainValue = _chart.domainAxis.getDomain(position.toDouble());
|
|
||||||
|
|
||||||
if (_domainCenterPoint != null) {
|
|
||||||
_domainCenterPoint = Point<int>(position.round(), _domainCenterPoint.y);
|
|
||||||
} else {
|
|
||||||
_domainCenterPoint = Point<int>(
|
|
||||||
position.round(), (viewBounds.top + viewBounds.height / 2).round());
|
|
||||||
}
|
|
||||||
|
|
||||||
num handleReferenceY;
|
|
||||||
switch (_style.handlePosition) {
|
|
||||||
case SliderHandlePosition.middle:
|
|
||||||
handleReferenceY = _domainCenterPoint.y;
|
|
||||||
break;
|
|
||||||
case SliderHandlePosition.top:
|
|
||||||
handleReferenceY = viewBounds.top;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw ArgumentError('Slider does not support the handle position '
|
|
||||||
'"${_style.handlePosition}"');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the slider handle along the domain axis.
|
|
||||||
_handleBounds = Rectangle<int>(
|
|
||||||
(_domainCenterPoint.x -
|
|
||||||
_style.handleSize.width / 2 +
|
|
||||||
_style.handleOffset.x)
|
|
||||||
.round(),
|
|
||||||
(handleReferenceY -
|
|
||||||
_style.handleSize.height / 2 +
|
|
||||||
_style.handleOffset.y)
|
|
||||||
.round(),
|
|
||||||
_style.handleSize.width,
|
|
||||||
_style.handleSize.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
return positionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves the slider along the domain axis to the location of [domain].
|
|
||||||
///
|
|
||||||
/// If [domain] exists beyond either edge of the draw area, the position will
|
|
||||||
/// be bound to the nearest edge.
|
|
||||||
///
|
|
||||||
/// Updates [_domainValue] with the location of [domain]. For ordinal axes,
|
|
||||||
/// this might result in a different domain value if the range band of
|
|
||||||
/// [domain] is completely outside of the viewport.
|
|
||||||
///
|
|
||||||
/// Updates [_domainCenterPoint] and [_handleBounds] with the new position of
|
|
||||||
/// the slider.
|
|
||||||
///
|
|
||||||
/// Returns whether or not the position actually changed. This will generally
|
|
||||||
/// be false if the mouse was dragged outside of the domain axis viewport.
|
|
||||||
bool _moveSliderToDomain(D domain) {
|
|
||||||
final x = _chart.domainAxis.getLocation(domain);
|
|
||||||
|
|
||||||
return _moveSliderToPoint(Point<double>(x, 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Programmatically moves the slider to the location of [domain] on the
|
|
||||||
/// domain axis.
|
|
||||||
///
|
|
||||||
/// If [domain] exists beyond either edge of the draw area, the position will
|
|
||||||
/// be bound to the nearest edge of the chart. The slider's current domain
|
|
||||||
/// value state will reflect the domain value at the edge of the chart. For
|
|
||||||
/// ordinal axes, this might result in a domain value whose range band is
|
|
||||||
/// partially located beyond the edge of the chart.
|
|
||||||
///
|
|
||||||
/// This does nothing if the domain matches the current domain location.
|
|
||||||
///
|
|
||||||
/// [SliderEventListener] callbacks will be fired to indicate that the slider
|
|
||||||
/// has moved.
|
|
||||||
///
|
|
||||||
/// [skipAnimation] controls whether or not the slider will animate. Animation
|
|
||||||
/// is disabled by default.
|
|
||||||
void moveSliderToDomain(D domain, {bool skipAnimation = true}) {
|
|
||||||
// Nothing to do if we are unattached to a chart or asked to move to the
|
|
||||||
// current location.
|
|
||||||
if (_chart == null || domain == _domainValue) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final positionChanged = _moveSliderToDomain(domain);
|
|
||||||
|
|
||||||
if (positionChanged) {
|
|
||||||
_dragStateToFireOnPostRender = SliderListenerDragState.end;
|
|
||||||
|
|
||||||
_chart.redraw(skipAnimation: skipAnimation, skipLayout: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
if (!(chart is CartesianChart)) {
|
|
||||||
throw ArgumentError('Slider can only be attached to a cartesian chart.');
|
|
||||||
}
|
|
||||||
|
|
||||||
_chart = chart as CartesianChart;
|
|
||||||
|
|
||||||
// Only vertical rendering is supported by this behavior.
|
|
||||||
assert(_chart.vertical);
|
|
||||||
|
|
||||||
_view = _SliderLayoutView<D>(
|
|
||||||
layoutPaintOrder: layoutPaintOrder, handleRenderer: _handleRenderer);
|
|
||||||
|
|
||||||
chart.addView(_view);
|
|
||||||
chart.addGestureListener(_gestureListener);
|
|
||||||
chart.addLifecycleListener(_lifecycleListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart<D> chart) {
|
|
||||||
chart.removeView(_view);
|
|
||||||
chart.removeGestureListener(_gestureListener);
|
|
||||||
chart.removeLifecycleListener(_lifecycleListener);
|
|
||||||
_chart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'Slider-${eventTrigger.toString()}-$_roleId';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Style configuration for a [Slider] behavior.
|
|
||||||
class SliderStyle {
|
|
||||||
/// Fill color of the handle of the slider.
|
|
||||||
Color fillColor;
|
|
||||||
|
|
||||||
/// Allows users to specify both x-position and y-position offset values that
|
|
||||||
/// determines where the slider handle will be rendered. The offset will be
|
|
||||||
/// calculated relative to its default position at the vertical and horizontal
|
|
||||||
/// center of the slider line.
|
|
||||||
Point<double> handleOffset;
|
|
||||||
|
|
||||||
/// The vertical position for the slider handle.
|
|
||||||
SliderHandlePosition handlePosition;
|
|
||||||
|
|
||||||
/// Specifies the size of the slider handle.
|
|
||||||
Rectangle<int> handleSize;
|
|
||||||
|
|
||||||
/// Stroke width of the slider line and the slider handle.
|
|
||||||
double strokeWidthPx;
|
|
||||||
|
|
||||||
/// Stroke color of the slider line and hte slider handle
|
|
||||||
Color strokeColor = StyleFactory.style.sliderStrokeColor;
|
|
||||||
|
|
||||||
SliderStyle(
|
|
||||||
{Color fillColor,
|
|
||||||
this.handleOffset = const Point<double>(0.0, 0.0),
|
|
||||||
this.handleSize = const Rectangle<int>(0, 0, 10, 20),
|
|
||||||
Color strokeColor,
|
|
||||||
this.handlePosition = SliderHandlePosition.middle,
|
|
||||||
this.strokeWidthPx = 2.0}) {
|
|
||||||
this.fillColor = fillColor ?? StyleFactory.style.sliderFillColor;
|
|
||||||
this.strokeColor = strokeColor ?? StyleFactory.style.sliderStrokeColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object o) {
|
|
||||||
return o is SliderStyle &&
|
|
||||||
fillColor == o.fillColor &&
|
|
||||||
handleOffset == o.handleOffset &&
|
|
||||||
handleSize == o.handleSize &&
|
|
||||||
strokeWidthPx == o.strokeWidthPx &&
|
|
||||||
strokeColor == o.strokeColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
int hashcode = fillColor?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + handleOffset?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + handleSize?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + strokeWidthPx?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + strokeColor?.hashCode ?? 0;
|
|
||||||
hashcode = (hashcode * 37) + handlePosition?.hashCode ?? 0;
|
|
||||||
return hashcode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes the vertical position of the slider handle on the slider.
|
|
||||||
///
|
|
||||||
/// [middle] indicates the handle should be half-way between the top and bottom
|
|
||||||
/// of the chart in the middle of the slider line.
|
|
||||||
///
|
|
||||||
/// [top] indicates the slider should be rendered relative to the top of the
|
|
||||||
/// chart.
|
|
||||||
enum SliderHandlePosition { middle, top }
|
|
||||||
|
|
||||||
/// Layout view component for [Slider].
|
|
||||||
class _SliderLayoutView<D> extends LayoutView {
|
|
||||||
final LayoutViewConfig layoutConfig;
|
|
||||||
|
|
||||||
Rectangle<int> _drawAreaBounds;
|
|
||||||
|
|
||||||
Rectangle<int> get drawBounds => _drawAreaBounds;
|
|
||||||
|
|
||||||
GraphicsFactory graphicsFactory;
|
|
||||||
|
|
||||||
/// Renderer for the handle. Defaults to a rectangle.
|
|
||||||
SymbolRenderer _handleRenderer;
|
|
||||||
|
|
||||||
/// Rendering data for the slider line and handle.
|
|
||||||
_AnimatedSlider _sliderHandle;
|
|
||||||
|
|
||||||
_SliderLayoutView(
|
|
||||||
{@required int layoutPaintOrder, @required SymbolRenderer handleRenderer})
|
|
||||||
: this.layoutConfig = LayoutViewConfig(
|
|
||||||
paintOrder: layoutPaintOrder,
|
|
||||||
position: LayoutPosition.DrawArea,
|
|
||||||
positionOrder: LayoutViewPositionOrder.drawArea),
|
|
||||||
_handleRenderer = handleRenderer;
|
|
||||||
|
|
||||||
set sliderHandle(_AnimatedSlider value) {
|
|
||||||
_sliderHandle = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ViewMeasuredSizes measure(int maxWidth, int maxHeight) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
|
|
||||||
this._drawAreaBounds = drawAreaBounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(ChartCanvas canvas, double animationPercent) {
|
|
||||||
final sliderElement = _sliderHandle.getCurrentSlider(animationPercent);
|
|
||||||
|
|
||||||
canvas.drawLine(
|
|
||||||
points: [
|
|
||||||
Point<num>(sliderElement.domainCenterPoint.x, _drawAreaBounds.top),
|
|
||||||
Point<num>(sliderElement.domainCenterPoint.x, _drawAreaBounds.bottom),
|
|
||||||
],
|
|
||||||
stroke: sliderElement.stroke,
|
|
||||||
strokeWidthPx: sliderElement.strokeWidthPx);
|
|
||||||
|
|
||||||
_handleRenderer.paint(canvas, sliderElement.buttonBounds,
|
|
||||||
fillColor: sliderElement.fill,
|
|
||||||
strokeColor: sliderElement.stroke,
|
|
||||||
strokeWidthPx: sliderElement.strokeWidthPx);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Rectangle<int> get componentBounds => this._drawAreaBounds;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isSeriesRenderer => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rendering information for a slider control element.
|
|
||||||
class _SliderElement<D> {
|
|
||||||
Point<int> domainCenterPoint;
|
|
||||||
Rectangle<int> buttonBounds;
|
|
||||||
Color fill;
|
|
||||||
Color stroke;
|
|
||||||
double strokeWidthPx;
|
|
||||||
|
|
||||||
_SliderElement<D> clone() {
|
|
||||||
return _SliderElement<D>()
|
|
||||||
..domainCenterPoint = this.domainCenterPoint
|
|
||||||
..buttonBounds = this.buttonBounds
|
|
||||||
..fill = this.fill
|
|
||||||
..stroke = this.stroke
|
|
||||||
..strokeWidthPx = this.strokeWidthPx;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateAnimationPercent(
|
|
||||||
_SliderElement previous, _SliderElement target, double animationPercent) {
|
|
||||||
final _SliderElement localPrevious = previous;
|
|
||||||
final _SliderElement localTarget = target;
|
|
||||||
|
|
||||||
final previousPoint = localPrevious.domainCenterPoint;
|
|
||||||
final targetPoint = localTarget.domainCenterPoint;
|
|
||||||
|
|
||||||
final x = ((targetPoint.x - previousPoint.x) * animationPercent) +
|
|
||||||
previousPoint.x;
|
|
||||||
|
|
||||||
final y = ((targetPoint.y - previousPoint.y) * animationPercent) +
|
|
||||||
previousPoint.y;
|
|
||||||
|
|
||||||
domainCenterPoint = Point<int>(x.round(), y.round());
|
|
||||||
|
|
||||||
final previousBounds = localPrevious.buttonBounds;
|
|
||||||
final targetBounds = localTarget.buttonBounds;
|
|
||||||
|
|
||||||
final top = ((targetBounds.top - previousBounds.top) * animationPercent) +
|
|
||||||
previousBounds.top;
|
|
||||||
final right =
|
|
||||||
((targetBounds.right - previousBounds.right) * animationPercent) +
|
|
||||||
previousBounds.right;
|
|
||||||
final bottom =
|
|
||||||
((targetBounds.bottom - previousBounds.bottom) * animationPercent) +
|
|
||||||
previousBounds.bottom;
|
|
||||||
final left =
|
|
||||||
((targetBounds.left - previousBounds.left) * animationPercent) +
|
|
||||||
previousBounds.left;
|
|
||||||
|
|
||||||
buttonBounds = Rectangle<int>(left.round(), top.round(),
|
|
||||||
(right - left).round(), (bottom - top).round());
|
|
||||||
|
|
||||||
fill = getAnimatedColor(previous.fill, target.fill, animationPercent);
|
|
||||||
|
|
||||||
stroke = getAnimatedColor(previous.stroke, target.stroke, animationPercent);
|
|
||||||
|
|
||||||
strokeWidthPx =
|
|
||||||
(((target.strokeWidthPx - previous.strokeWidthPx) * animationPercent) +
|
|
||||||
previous.strokeWidthPx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animates the slider control element of the behavior between different
|
|
||||||
/// states.
|
|
||||||
class _AnimatedSlider<D> {
|
|
||||||
_SliderElement<D> _previousSlider;
|
|
||||||
_SliderElement<D> _targetSlider;
|
|
||||||
_SliderElement<D> _currentSlider;
|
|
||||||
|
|
||||||
// Flag indicating whether this point is being animated out of the chart.
|
|
||||||
bool animatingOut = false;
|
|
||||||
|
|
||||||
_AnimatedSlider();
|
|
||||||
|
|
||||||
/// Animates a point that was removed from the series out of the view.
|
|
||||||
///
|
|
||||||
/// This should be called in place of "setNewTarget" for points that represent
|
|
||||||
/// data that has been removed from the series.
|
|
||||||
///
|
|
||||||
/// Animates the width of the slider down to 0.
|
|
||||||
void animateOut() {
|
|
||||||
final newTarget = _currentSlider.clone();
|
|
||||||
|
|
||||||
// Animate the button bounds inwards horizontally towards a 0 width box.
|
|
||||||
final targetBounds = newTarget.buttonBounds;
|
|
||||||
final top = targetBounds.top;
|
|
||||||
final right = targetBounds.left + targetBounds.width / 2;
|
|
||||||
final bottom = targetBounds.bottom;
|
|
||||||
final left = right;
|
|
||||||
|
|
||||||
newTarget.buttonBounds = Rectangle<int>(left.round(), top.round(),
|
|
||||||
(right - left).round(), (bottom - top).round());
|
|
||||||
|
|
||||||
// Animate the stroke width to 0 so that we don't get a lingering line after
|
|
||||||
// animation is done.
|
|
||||||
newTarget.strokeWidthPx = 0.0;
|
|
||||||
|
|
||||||
setNewTarget(newTarget);
|
|
||||||
animatingOut = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setNewTarget(_SliderElement<D> newTarget) {
|
|
||||||
animatingOut = false;
|
|
||||||
_currentSlider ??= newTarget.clone();
|
|
||||||
_previousSlider = _currentSlider.clone();
|
|
||||||
_targetSlider = newTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
_SliderElement<D> getCurrentSlider(double animationPercent) {
|
|
||||||
if (animationPercent == 1.0 || _previousSlider == null) {
|
|
||||||
_currentSlider = _targetSlider;
|
|
||||||
_previousSlider = _targetSlider;
|
|
||||||
return _currentSlider;
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentSlider.updateAnimationPercent(
|
|
||||||
_previousSlider, _targetSlider, animationPercent);
|
|
||||||
|
|
||||||
return _currentSlider;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event handler for slider events.
|
|
||||||
class SliderEventListener<D> {
|
|
||||||
/// Called when the position of the slider has changed during a drag event.
|
|
||||||
final SliderListenerCallback<D> onChange;
|
|
||||||
|
|
||||||
SliderEventListener({this.onChange});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Callback function for [Slider] drag events.
|
|
||||||
///
|
|
||||||
/// [point] is the current position of the slider line. [point.x] is the domain
|
|
||||||
/// position, and [point.y] is the position of the center of the line on the
|
|
||||||
/// measure axis.
|
|
||||||
///
|
|
||||||
/// [domain] is the domain value at the slider position.
|
|
||||||
///
|
|
||||||
/// [dragState] indicates the current state of a drag event.
|
|
||||||
typedef SliderListenerCallback<D> = Function(Point<int> point, D domain,
|
|
||||||
String roleId, SliderListenerDragState dragState);
|
|
||||||
|
|
||||||
/// Describes the current state of a slider change as a result of a drag event.
|
|
||||||
///
|
|
||||||
/// [initial] indicates that the slider was set to an initial position when new
|
|
||||||
/// data was drawn on a chart. This will be fired if an initialDomainValue is
|
|
||||||
/// passed to [Slider]. It will also be fired if the position of the slider
|
|
||||||
/// changes as a result of new data being drawn on the chart.
|
|
||||||
///
|
|
||||||
/// [drag] indicates that the slider is being moved as a result of drag events.
|
|
||||||
/// When this is passed, the drag event is still active. Once the drag event is
|
|
||||||
/// completed, an [end] event will be fired.
|
|
||||||
///
|
|
||||||
/// [end] indicates that a drag event has been completed. This usually occurs
|
|
||||||
/// after one or more [drag] events. An [end] event will also be fired if
|
|
||||||
/// [Slider.moveSliderToDomain] is called, but there will be no preceding [drag]
|
|
||||||
/// events in this case.
|
|
||||||
enum SliderListenerDragState { initial, drag, end }
|
|
||||||
|
|
||||||
/// Helper class that exposes fewer private internal properties for unit tests.
|
|
||||||
@visibleForTesting
|
|
||||||
class SliderTester<D> {
|
|
||||||
final Slider<D> behavior;
|
|
||||||
|
|
||||||
SliderTester(this.behavior);
|
|
||||||
|
|
||||||
Point<int> get domainCenterPoint => behavior._domainCenterPoint;
|
|
||||||
|
|
||||||
D get domainValue => behavior._domainValue;
|
|
||||||
|
|
||||||
Rectangle<int> get handleBounds => behavior._handleBounds;
|
|
||||||
|
|
||||||
void layout(Rectangle<int> componentBounds, Rectangle<int> drawAreaBounds) {
|
|
||||||
behavior._view.layout(componentBounds, drawAreaBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
_SliderLayoutView get view => behavior._view;
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import '../../cartesian/cartesian_chart.dart' show CartesianChart;
|
|
||||||
import '../base_chart.dart' show BaseChart;
|
|
||||||
import '../selection_model/selection_model.dart'
|
|
||||||
show SelectionModel, SelectionModelType;
|
|
||||||
import 'chart_behavior.dart' show ChartBehavior;
|
|
||||||
|
|
||||||
/// Chart behavior that centers the viewport on the selected domain.
|
|
||||||
///
|
|
||||||
/// It is used in combination with SelectNearest to update the selection model
|
|
||||||
/// and notify this behavior to update the viewport on selection change.
|
|
||||||
///
|
|
||||||
/// This behavior can only be used on [CartesianChart].
|
|
||||||
class SlidingViewport<D> implements ChartBehavior<D> {
|
|
||||||
final SelectionModelType selectionModelType;
|
|
||||||
|
|
||||||
CartesianChart<D> _chart;
|
|
||||||
|
|
||||||
SlidingViewport([this.selectionModelType = SelectionModelType.info]);
|
|
||||||
|
|
||||||
void _selectionChanged(SelectionModel selectionModel) {
|
|
||||||
if (selectionModel.hasAnySelection == false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate current viewport center and determine the translate pixels
|
|
||||||
// needed based on the selected domain value's location and existing amount
|
|
||||||
// of translate pixels.
|
|
||||||
final domainAxis = _chart.domainAxis;
|
|
||||||
final selectedDatum = selectionModel.selectedDatum.first;
|
|
||||||
final domainLocation = domainAxis
|
|
||||||
.getLocation(selectedDatum.series.domainFn(selectedDatum.index));
|
|
||||||
final viewportCenter =
|
|
||||||
domainAxis.range.start + (domainAxis.range.width / 2);
|
|
||||||
final translatePx =
|
|
||||||
domainAxis.viewportTranslatePx + (viewportCenter - domainLocation);
|
|
||||||
domainAxis.setViewportSettings(
|
|
||||||
domainAxis.viewportScalingFactor, translatePx);
|
|
||||||
|
|
||||||
_chart.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void attachTo(BaseChart<D> chart) {
|
|
||||||
assert(chart is CartesianChart);
|
|
||||||
_chart = chart as CartesianChart<D>;
|
|
||||||
chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.addSelectionChangedListener(_selectionChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void removeFrom(BaseChart chart) {
|
|
||||||
chart
|
|
||||||
.getSelectionModel(selectionModelType)
|
|
||||||
.removeSelectionChangedListener(_selectionChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'slidingViewport-${selectionModelType.toString()}';
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
// Copyright 2018 the Charts project authors. Please see the AUTHORS file
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
import 'dart:math' show Point;
|
|
||||||
|
|
||||||
import 'package:meta/meta.dart' show protected;
|
|
||||||
|
|
||||||
import '../../../../common/gesture_listener.dart' show GestureListener;
|
|
||||||
import '../../../cartesian/axis/axis.dart' show Axis;
|
|
||||||
import '../../../cartesian/cartesian_chart.dart' show CartesianChart;
|
|
||||||
import '../../base_chart.dart' show BaseChart, LifecycleListener;
|
|
||||||
import '../chart_behavior.dart' show ChartBehavior;
|
|
||||||
|
|
||||||
/// Adds initial hint behavior for [CartesianChart].
|
|
||||||
///
|
|
||||||
/// This behavior animates to the final viewport from an initial translate and
|
|
||||||
/// or scale factor.
|
|
||||||
abstract class InitialHintBehavior<D> implements ChartBehavior<D> {
|
|
||||||
/// Listens for drag gestures.
|
|
||||||
GestureListener _listener;
|
|
||||||
|
|
||||||
/// Chart lifecycle listener to setup hint animation.
|
|
||||||
LifecycleListener<D> _lifecycleListener;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get role => 'InitialHint';
|
|
||||||
|
|
||||||
/// The chart to which the behavior is attached.
|
|
||||||
CartesianChart<D> _chart;
|
|
||||||
|
|
||||||
@protected
|
|
||||||
CartesianChart<D> get chart => _chart;
|
|
||||||
|
|
||||||
Duration _hintDuration = Duration(milliseconds: 3000);
|
|
||||||
|
|
||||||
/// The amount of time to animate to the desired viewport.
|
|
||||||
///
|
|
||||||
/// If no duration is passed in, the default of 3000 ms is used.
|
|
||||||
@protected
|
|
||||||
Duration get hintDuration => _hintDuration;
|
|
||||||
|
|
||||||
set hintDuration(Duration duration) {
|
|
||||||
_hintDuration = duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _maxHintTranslate = 0.0;
|
|
||||||
|
|
||||||
// TODO: Translation animation only works for ordinal axis.
|
|
||||||
/// The maximum amount ordinal values to shift the viewport for the the hint
|
|
||||||
/// animation.
|
|
||||||
///
|
|
||||||
/// Positive numbers shift the viewport to the right and negative to the left.
|
|
||||||
/// The default is no translation.
|
|
||||||
@protected
|
|
||||||
double get maxHintTranslate => _maxHintTranslate;
|
|
||||||
|
|
||||||
set maxHintTranslate(double maxHintTranslate) {
|
|
||||||
_maxHintTranslate = maxHintTranslate;
|
|
||||||
}
|
|
||||||
|
|
||||||
double _maxHintScaleFactor;
|
|
||||||
|
|
||||||
/// The amount the domain axis will be scaled for the start of the hint.
|
|
||||||
///
|
|
||||||
/// A value of 1.0 means the viewport is completely zoomed out (all domains
|
|
||||||
/// are in the viewport). If a value is provided, it cannot be less than 1.0.
|
|
||||||
///
|
|
||||||
/// By default maxHintScaleFactor is not set.
|
|
||||||
@protected
|
|
||||||
double get maxHintScaleFactor => _maxHintScaleFactor;
|
|
||||||
|
|
||||||
set maxHintScaleFactor(double maxHintScaleFactor) {
|
|
||||||
assert(maxHintScaleFactor != null && maxHintScaleFactor >= 1.0);
|
|
||||||
|
|
||||||
_maxHintScaleFactor = maxHintScaleFactor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flag to indicate that hint animation controller has already been set up.
|
|
||||||
///
|
|
||||||
/// This is to ensure that the hint is only set up on the first draw.
|
|
||||||
bool _hintSetupCompleted = false;
|
|
||||||
|
|
||||||
/// Flag to indicate that the first call to axis configured is completed.
|
|
||||||
///
|
|
||||||
/// This is to ensure that the initial and target viewport translate and scale
|
|
||||||
/// factor is only calculated on the first axis configuration.
|
|
||||||
bool _firstAxisConfigured = false;
|
|
||||||
|
|
||||||
double _initialViewportTranslatePx;
|
|
||||||
double _initialViewportScalingFactor;
|
|
||||||
double _targetViewportTranslatePx;
|
|
||||||
double _targetViewportScalingFactor;
|
|
||||||
|
|
||||||
InitialHintBehavior() {
|
|
||||||
_listener = GestureListener(onTapTest: onTapTest);
|
|
||||||
|
|
||||||
_lifecycleListener = LifecycleListener<D>(
|
|
||||||
onAxisConfigured: _onAxisConfigured,
|
|
||||||
onAnimationComplete: _onAnimationComplete);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
attachTo(BaseChart<D> chart) {
|
|
||||||
if (!(chart is CartesianChart)) {
|
|
||||||
throw ArgumentError(
|
|
||||||
'InitialHintBehavior can only be attached to a CartesianChart');
|
|
||||||
}
|
|
||||||
|
|
||||||
_chart = chart;
|
|
||||||
|
|
||||||
_chart.addGestureListener(_listener);
|
|
||||||
_chart.addLifecycleListener(_lifecycleListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
removeFrom(BaseChart<D> chart) {
|
|
||||||
if (!(chart is CartesianChart)) {
|
|
||||||
throw ArgumentError(
|
|
||||||
'InitialHintBehavior can only be removed from a CartesianChart');
|
|
||||||
}
|
|
||||||
|
|
||||||
stopHintAnimation();
|
|
||||||
|
|
||||||
_chart = chart;
|
|
||||||
_chart.removeGestureListener(_listener);
|
|
||||||
_chart.removeLifecycleListener(_lifecycleListener);
|
|
||||||
|
|
||||||
_chart = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@protected
|
|
||||||
bool onTapTest(Point<double> localPosition) {
|
|
||||||
if (_chart == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the user taps the chart, stop the hint animation immediately.
|
|
||||||
stopHintAnimation();
|
|
||||||
|
|
||||||
return _chart.withinDrawArea(localPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate the animation's initial and target viewport and scale factor
|
|
||||||
/// and shift the viewport to the start.
|
|
||||||
void _onAxisConfigured() {
|
|
||||||
if (_firstAxisConfigured == false) {
|
|
||||||
_firstAxisConfigured = true;
|
|
||||||
|
|
||||||
final domainAxis = chart.domainAxis;
|
|
||||||
|
|
||||||
// TODO: Translation animation only works for axis with a
|
|
||||||
// rangeband type that returns a non zero step size. If two rows have
|
|
||||||
// the same domain value, step size could also equal 0.
|
|
||||||
assert(domainAxis.stepSize != 0.0);
|
|
||||||
|
|
||||||
// Save the target viewport and scale factor from axis, because the
|
|
||||||
// viewport can be set by the user using AxisSpec.
|
|
||||||
_targetViewportTranslatePx = domainAxis.viewportTranslatePx;
|
|
||||||
_targetViewportScalingFactor = domainAxis.viewportScalingFactor;
|
|
||||||
|
|
||||||
// Calculate the amount to translate from the target viewport.
|
|
||||||
final translateAmount = domainAxis.stepSize * maxHintTranslate;
|
|
||||||
|
|
||||||
_initialViewportTranslatePx =
|
|
||||||
_targetViewportTranslatePx - translateAmount;
|
|
||||||
|
|
||||||
_initialViewportScalingFactor =
|
|
||||||
maxHintScaleFactor ?? _targetViewportScalingFactor;
|
|
||||||
|
|
||||||
domainAxis.setViewportSettings(
|
|
||||||
_initialViewportScalingFactor, _initialViewportTranslatePx);
|
|
||||||
chart.redraw(skipAnimation: true, skipLayout: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start the hint animation, only start the animation on the very first draw.
|
|
||||||
void _onAnimationComplete() {
|
|
||||||
if (_hintSetupCompleted == false) {
|
|
||||||
_hintSetupCompleted = true;
|
|
||||||
|
|
||||||
startHintAnimation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Setup and start the hint animation.
|
|
||||||
///
|
|
||||||
/// Animation controller to be handled by the native platform.
|
|
||||||
@protected
|
|
||||||
void startHintAnimation() {
|
|
||||||
// When panning starts, measure tick provider should not update ticks.
|
|
||||||
// This is still needed because axis internally updates the tick location
|
|
||||||
// after the tick provider generates the ticks. If we do not tell the axis
|
|
||||||
// not to update the location of the measure axes, the measure axis will
|
|
||||||
// change during the hint animation and make values jump back and forth.
|
|
||||||
_chart.getMeasureAxis().lockAxis = true;
|
|
||||||
_chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stop hint animation
|
|
||||||
@protected
|
|
||||||
void stopHintAnimation() {
|
|
||||||
// When panning is completed, unlock the measure axis.
|
|
||||||
_chart.getMeasureAxis().lockAxis = false;
|
|
||||||
_chart.getMeasureAxis(axisId: Axis.secondaryMeasureAxisId)?.lockAxis =
|
|
||||||
false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Animation hint percent, to be returned by the native platform.
|
|
||||||
@protected
|
|
||||||
double get hintAnimationPercent;
|
|
||||||
|
|
||||||
/// Shift domain viewport on hint animation ticks.
|
|
||||||
@protected
|
|
||||||
void onHintTick() {
|
|
||||||
final percent = hintAnimationPercent;
|
|
||||||
|
|
||||||
final scaleFactor = _lerpDouble(
|
|
||||||
_initialViewportScalingFactor, _targetViewportScalingFactor, percent);
|
|
||||||
|
|
||||||
double translatePx = _lerpDouble(
|
|
||||||
_initialViewportTranslatePx, _targetViewportTranslatePx, percent);
|
|
||||||
|
|
||||||
// If there is a scale factor animation, need to scale the translatePx so
|
|
||||||
// the animation appears to be zooming in on the viewport when there is no
|
|
||||||
// [maxHintTranslate] provided.
|
|
||||||
//
|
|
||||||
// If there is a translate hint, the animation will still first zoom in
|
|
||||||
// and then translate the [maxHintTranslate] amount.
|
|
||||||
if (_initialViewportScalingFactor != _targetViewportScalingFactor) {
|
|
||||||
translatePx = translatePx * percent;
|
|
||||||
}
|
|
||||||
|
|
||||||
final domainAxis = chart.domainAxis;
|
|
||||||
domainAxis.setViewportSettings(scaleFactor, translatePx,
|
|
||||||
drawAreaWidth: chart.drawAreaBounds.width);
|
|
||||||
|
|
||||||
if (percent >= 1.0) {
|
|
||||||
stopHintAnimation();
|
|
||||||
chart.redraw();
|
|
||||||
} else {
|
|
||||||
chart.redraw(skipAnimation: true, skipLayout: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Linear interpolation for doubles.
|
|
||||||
double _lerpDouble(double a, double b, double t) {
|
|
||||||
if (a == null && b == null) return null;
|
|
||||||
a ??= 0.0;
|
|
||||||
b ??= 0.0;
|
|
||||||
return a + (b - a) * t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user