mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +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
|
||||
*/build
|
||||
|
||||
@@ -110,7 +110,7 @@ class _Demo {
|
||||
String get html => '''
|
||||
<div>
|
||||
<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 class='demo-title' href='$buildDir'>$name</a>
|
||||
<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