From f7c93b1eb3f1446b5b018c273d4d98597e3a14d6 Mon Sep 17 00:00:00 2001 From: padmanto Date: Thu, 3 Apr 2025 14:27:30 +0700 Subject: [PATCH] first commit --- CHANGELOG.md | 79 +++ LICENSE | 202 +++++++ charts_flutter.gwsq | 6 + example/android.iml | 12 + example/android/Android_Charts.xml | 7 + example/android/app/build.gradle | 51 ++ .../android/app/src/main/AndroidManifest.xml | 39 ++ .../com/example/examples/MainActivity.java | 17 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values/styles.xml | 8 + example/android/build.gradle | 29 + example/android/gradle.properties | 4 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + example/android/gradlew | 160 +++++ example/android/gradlew.bat | 90 +++ example/android/settings.gradle | 15 + example/examples.iml | 17 + example/examples_android.iml | 26 + example/ios/Flutter/AppFrameworkInfo.plist | 30 + example/ios/Flutter/Debug.xcconfig | 1 + example/ios/Flutter/Release.xcconfig | 1 + example/ios/Ios_Charts.xml | 7 + example/ios/Runner.xcodeproj/project.pbxproj | 436 ++++++++++++++ .../contents.xcworkspacedata | 10 + .../xcshareddata/xcschemes/Runner.xcscheme | 91 +++ example/ios/Runner/AppDelegate.h | 22 + example/ios/Runner/AppDelegate.m | 58 ++ .../AppIcon.appiconset/Contents.json | 82 +++ .../AppIcon.appiconset/Icon-60@2x.png | Bin 0 -> 4977 bytes .../AppIcon.appiconset/Icon-60@3x.png | Bin 0 -> 7905 bytes .../AppIcon.appiconset/Icon-76.png | Bin 0 -> 3279 bytes .../AppIcon.appiconset/Icon-76@2x.png | Bin 0 -> 6400 bytes .../AppIcon.appiconset/Icon-83.5@2x.png | Bin 0 -> 7160 bytes .../AppIcon.appiconset/Icon-Small-40.png | Bin 0 -> 1769 bytes .../AppIcon.appiconset/Icon-Small-40@2x.png | Bin 0 -> 3417 bytes .../AppIcon.appiconset/Icon-Small-40@3x.png | Bin 0 -> 4977 bytes .../AppIcon.appiconset/Icon-Small.png | Bin 0 -> 1298 bytes .../AppIcon.appiconset/Icon-Small@2x.png | Bin 0 -> 2528 bytes .../AppIcon.appiconset/Icon-Small@3x.png | Bin 0 -> 3790 bytes .../Runner/Base.lproj/LaunchScreen.storyboard | 27 + example/ios/Runner/Base.lproj/Main.storyboard | 26 + example/ios/Runner/Info.plist | 47 ++ example/ios/Runner/main.m | 24 + example/lib/a11y/a11y_gallery.dart | 29 + .../a11y/domain_a11y_explore_bar_chart.dart | 215 +++++++ example/lib/app_config.dart | 44 ++ example/lib/axes/axes_gallery.dart | 137 +++++ example/lib/axes/bar_secondary_axis.dart | 158 +++++ example/lib/axes/bar_secondary_axis_only.dart | 114 ++++ .../lib/axes/custom_axis_tick_formatters.dart | 144 +++++ .../lib/axes/custom_font_size_and_color.dart | 136 +++++ .../lib/axes/custom_measure_tick_count.dart | 122 ++++ example/lib/axes/flipped_vertical_axis.dart | 114 ++++ example/lib/axes/gridline_dash_pattern.dart | 120 ++++ .../axes/hidden_ticks_and_labels_axis.dart | 118 ++++ .../axes/horizontal_bar_secondary_axis.dart | 160 +++++ .../lib/axes/integer_only_measure_axis.dart | 129 ++++ example/lib/axes/line_disjoint_axis.dart | 268 +++++++++ .../axes/measure_axis_label_alignment.dart | 122 ++++ .../lib/axes/nonzero_bound_measure_axis.dart | 119 ++++ .../lib/axes/numeric_initial_viewport.dart | 133 +++++ .../lib/axes/ordinal_initial_viewport.dart | 145 +++++ example/lib/axes/short_tick_length_axis.dart | 115 ++++ .../lib/axes/statically_provided_ticks.dart | 134 +++++ example/lib/bar_chart/bar_gallery.dart | 164 +++++ .../lib/bar_chart/custom_rounded_bars.dart | 110 ++++ example/lib/bar_chart/grouped.dart | 154 +++++ example/lib/bar_chart/grouped_fill_color.dart | 178 ++++++ .../bar_chart/grouped_single_target_line.dart | 180 ++++++ example/lib/bar_chart/grouped_stacked.dart | 244 ++++++++ .../grouped_stacked_weight_pattern.dart | 256 ++++++++ .../lib/bar_chart/grouped_target_line.dart | 248 ++++++++ example/lib/bar_chart/horizontal.dart | 104 ++++ .../lib/bar_chart/horizontal_bar_label.dart | 123 ++++ .../horizontal_bar_label_custom.dart | 140 +++++ .../horizontal_pattern_forward_hatch.dart | 164 +++++ .../lib/bar_chart/pattern_forward_hatch.dart | 161 +++++ example/lib/bar_chart/simple.dart | 104 ++++ example/lib/bar_chart/spark_bar.dart | 140 +++++ example/lib/bar_chart/stacked.dart | 155 +++++ example/lib/bar_chart/stacked_fill_color.dart | 178 ++++++ example/lib/bar_chart/stacked_horizontal.dart | 157 +++++ .../lib/bar_chart/stacked_target_line.dart | 249 ++++++++ example/lib/bar_chart/vertical_bar_label.dart | 120 ++++ example/lib/behaviors/behaviors_gallery.dart | 126 ++++ example/lib/behaviors/chart_title.dart | 131 ++++ .../lib/behaviors/initial_hint_animation.dart | 175 ++++++ example/lib/behaviors/initial_selection.dart | 129 ++++ example/lib/behaviors/percent_of_domain.dart | 167 ++++++ .../percent_of_domain_by_category.dart | 261 ++++++++ example/lib/behaviors/percent_of_series.dart | 120 ++++ .../behaviors/selection_bar_highlight.dart | 110 ++++ .../behaviors/selection_callback_example.dart | 201 +++++++ .../behaviors/selection_line_highlight.dart | 127 ++++ ...selection_line_highlight_custom_shape.dart | 130 ++++ .../selection_scatter_plot_highlight.dart | 235 ++++++++ .../lib/behaviors/selection_user_managed.dart | 164 +++++ example/lib/behaviors/slider.dart | 196 ++++++ .../sliding_viewport_on_selection.dart | 144 +++++ example/lib/combo_chart/combo_gallery.dart | 57 ++ .../lib/combo_chart/date_time_line_point.dart | 183 ++++++ example/lib/combo_chart/numeric_line_bar.dart | 174 ++++++ .../lib/combo_chart/numeric_line_point.dart | 175 ++++++ example/lib/combo_chart/ordinal_bar_line.dart | 166 ++++++ .../lib/combo_chart/scatter_plot_line.dart | 198 +++++++ example/lib/drawer.dart | 51 ++ example/lib/gallery_scaffold.dart | 66 +++ example/lib/home.dart | 127 ++++ example/lib/i18n/i18n_gallery.dart | 50 ++ example/lib/i18n/rtl_bar_chart.dart | 119 ++++ example/lib/i18n/rtl_line_chart.dart | 115 ++++ example/lib/i18n/rtl_line_segments.dart | 248 ++++++++ example/lib/i18n/rtl_series_legend.dart | 206 +++++++ example/lib/legends/datum_legend_options.dart | 136 +++++ .../legends/datum_legend_with_measures.dart | 146 +++++ .../legends/default_hidden_series_legend.dart | 188 ++++++ example/lib/legends/legend_custom_symbol.dart | 210 +++++++ example/lib/legends/legends_gallery.dart | 80 +++ .../lib/legends/series_legend_options.dart | 215 +++++++ .../legends/series_legend_with_measures.dart | 228 +++++++ example/lib/legends/simple_datum_legend.dart | 104 ++++ example/lib/legends/simple_series_legend.dart | 183 ++++++ example/lib/line_chart/animation_zoom.dart | 101 ++++ example/lib/line_chart/area_and_line.dart | 141 +++++ example/lib/line_chart/dash_pattern.dart | 162 +++++ example/lib/line_chart/line_annotation.dart | 124 ++++ example/lib/line_chart/line_gallery.dart | 113 ++++ example/lib/line_chart/points.dart | 103 ++++ example/lib/line_chart/range_annotation.dart | 124 ++++ .../line_chart/range_annotation_margin.dart | 141 +++++ example/lib/line_chart/segments.dart | 233 ++++++++ example/lib/line_chart/simple.dart | 101 ++++ example/lib/line_chart/simple_nulls.dart | 179 ++++++ example/lib/line_chart/stacked_area.dart | 160 +++++ .../line_chart/stacked_area_custom_color.dart | 175 ++++++ .../lib/line_chart/stacked_area_nulls.dart | 191 ++++++ example/lib/main.dart | 54 ++ example/lib/pie_chart/auto_label.dart | 123 ++++ example/lib/pie_chart/donut.dart | 103 ++++ example/lib/pie_chart/gauge.dart | 106 ++++ example/lib/pie_chart/outside_label.dart | 118 ++++ example/lib/pie_chart/partial_pie.dart | 104 ++++ example/lib/pie_chart/pie_gallery.dart | 65 ++ example/lib/pie_chart/simple.dart | 99 ++++ .../scatter_plot_chart/animation_zoom.dart | 144 +++++ .../scatter_plot_chart/bucketing_axis.dart | 264 +++++++++ .../scatter_plot_chart/comparison_points.dart | 169 ++++++ .../scatter_plot_gallery.dart | 58 ++ example/lib/scatter_plot_chart/shapes.dart | 206 +++++++ example/lib/scatter_plot_chart/simple.dart | 150 +++++ .../confidence_interval.dart | 119 ++++ .../time_series_chart/end_points_axis.dart | 111 ++++ .../time_series_chart/line_annotation.dart | 115 ++++ .../time_series_chart/range_annotation.dart | 111 ++++ .../range_annotation_margin.dart | 139 +++++ example/lib/time_series_chart/simple.dart | 108 ++++ .../time_series_chart/symbol_annotation.dart | 298 ++++++++++ .../time_series_gallery.dart | 80 +++ .../time_series_chart/with_bar_renderer.dart | 148 +++++ example/pubspec.yaml | 18 + lib/flutter.dart | 199 +++++++ lib/src/bar_chart.dart | 104 ++++ lib/src/base_chart.dart | 279 +++++++++ lib/src/base_chart_state.dart | 186 ++++++ .../a11y/domain_a11y_explore_behavior.dart | 116 ++++ .../calculation/percent_injector.dart | 67 +++ lib/src/behaviors/chart_behavior.dart | 72 +++ .../behaviors/chart_title/chart_title.dart | 202 +++++++ lib/src/behaviors/domain_highlighter.dart | 54 ++ lib/src/behaviors/initial_selection.dart | 69 +++ lib/src/behaviors/legend/datum_legend.dart | 341 +++++++++++ lib/src/behaviors/legend/legend.dart | 22 + .../legend/legend_content_builder.dart | 92 +++ .../behaviors/legend/legend_entry_layout.dart | 146 +++++ lib/src/behaviors/legend/legend_layout.dart | 162 +++++ lib/src/behaviors/legend/series_legend.dart | 383 ++++++++++++ lib/src/behaviors/line_point_highlighter.dart | 128 ++++ lib/src/behaviors/range_annotation.dart | 128 ++++ lib/src/behaviors/select_nearest.dart | 154 +++++ lib/src/behaviors/slider/slider.dart | 198 +++++++ lib/src/behaviors/sliding_viewport.dart | 53 ++ .../behaviors/zoom/initial_hint_behavior.dart | 131 ++++ .../behaviors/zoom/pan_and_zoom_behavior.dart | 64 ++ lib/src/behaviors/zoom/pan_behavior.dart | 186 ++++++ lib/src/canvas/circle_sector_painter.dart | 103 ++++ lib/src/canvas/line_painter.dart | 244 ++++++++ lib/src/canvas/pie_painter.dart | 84 +++ lib/src/canvas/point_painter.dart | 56 ++ lib/src/canvas/polygon_painter.dart | 99 ++++ lib/src/cartesian_chart.dart | 122 ++++ lib/src/chart_canvas.dart | 442 ++++++++++++++ lib/src/chart_container.dart | 379 ++++++++++++ lib/src/chart_gesture_detector.dart | 140 +++++ lib/src/chart_state.dart | 36 ++ lib/src/combo_chart/combo_chart.dart | 123 ++++ lib/src/graphics_factory.dart | 55 ++ lib/src/line_chart.dart | 90 +++ lib/src/line_style.dart | 29 + lib/src/pie_chart.dart | 49 ++ lib/src/scatter_plot_chart.dart | 82 +++ lib/src/selection_model_config.dart | 34 ++ lib/src/symbol_renderer.dart | 116 ++++ lib/src/text_element.dart | 186 ++++++ lib/src/text_style.dart | 38 ++ lib/src/time_series_chart.dart | 97 +++ lib/src/user_managed_state.dart | 77 +++ lib/src/util.dart | 45 ++ lib/src/util/color.dart | 28 + lib/src/widget_layout_delegate.dart | 222 +++++++ minimum_os.bzl | 3 + pubspec.yaml | 33 ++ test/behaviors/legend/legend_layout_test.dart | 117 ++++ test/text_element_test.dart | 54 ++ test/user_managed_state_test.dart | 137 +++++ test/widget_layout_delegate_test.dart | 561 ++++++++++++++++++ 221 files changed, 26063 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 charts_flutter.gwsq create mode 100644 example/android.iml create mode 100644 example/android/Android_Charts.xml create mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/src/main/AndroidManifest.xml create mode 100644 example/android/app/src/main/java/com/example/examples/MainActivity.java create mode 100644 example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/values/styles.xml create mode 100644 example/android/build.gradle create mode 100644 example/android/gradle.properties create mode 100644 example/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 example/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 example/android/gradlew create mode 100644 example/android/gradlew.bat create mode 100644 example/android/settings.gradle create mode 100644 example/examples.iml create mode 100644 example/examples_android.iml create mode 100644 example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 example/ios/Flutter/Debug.xcconfig create mode 100644 example/ios/Flutter/Release.xcconfig create mode 100644 example/ios/Ios_Charts.xml create mode 100644 example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 example/ios/Runner/AppDelegate.h create mode 100644 example/ios/Runner/AppDelegate.m create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png create mode 100644 example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 example/ios/Runner/Info.plist create mode 100644 example/ios/Runner/main.m create mode 100644 example/lib/a11y/a11y_gallery.dart create mode 100644 example/lib/a11y/domain_a11y_explore_bar_chart.dart create mode 100644 example/lib/app_config.dart create mode 100644 example/lib/axes/axes_gallery.dart create mode 100644 example/lib/axes/bar_secondary_axis.dart create mode 100644 example/lib/axes/bar_secondary_axis_only.dart create mode 100644 example/lib/axes/custom_axis_tick_formatters.dart create mode 100644 example/lib/axes/custom_font_size_and_color.dart create mode 100644 example/lib/axes/custom_measure_tick_count.dart create mode 100644 example/lib/axes/flipped_vertical_axis.dart create mode 100644 example/lib/axes/gridline_dash_pattern.dart create mode 100644 example/lib/axes/hidden_ticks_and_labels_axis.dart create mode 100644 example/lib/axes/horizontal_bar_secondary_axis.dart create mode 100644 example/lib/axes/integer_only_measure_axis.dart create mode 100644 example/lib/axes/line_disjoint_axis.dart create mode 100644 example/lib/axes/measure_axis_label_alignment.dart create mode 100644 example/lib/axes/nonzero_bound_measure_axis.dart create mode 100644 example/lib/axes/numeric_initial_viewport.dart create mode 100644 example/lib/axes/ordinal_initial_viewport.dart create mode 100644 example/lib/axes/short_tick_length_axis.dart create mode 100644 example/lib/axes/statically_provided_ticks.dart create mode 100644 example/lib/bar_chart/bar_gallery.dart create mode 100644 example/lib/bar_chart/custom_rounded_bars.dart create mode 100644 example/lib/bar_chart/grouped.dart create mode 100644 example/lib/bar_chart/grouped_fill_color.dart create mode 100644 example/lib/bar_chart/grouped_single_target_line.dart create mode 100644 example/lib/bar_chart/grouped_stacked.dart create mode 100644 example/lib/bar_chart/grouped_stacked_weight_pattern.dart create mode 100644 example/lib/bar_chart/grouped_target_line.dart create mode 100644 example/lib/bar_chart/horizontal.dart create mode 100644 example/lib/bar_chart/horizontal_bar_label.dart create mode 100644 example/lib/bar_chart/horizontal_bar_label_custom.dart create mode 100644 example/lib/bar_chart/horizontal_pattern_forward_hatch.dart create mode 100644 example/lib/bar_chart/pattern_forward_hatch.dart create mode 100644 example/lib/bar_chart/simple.dart create mode 100644 example/lib/bar_chart/spark_bar.dart create mode 100644 example/lib/bar_chart/stacked.dart create mode 100644 example/lib/bar_chart/stacked_fill_color.dart create mode 100644 example/lib/bar_chart/stacked_horizontal.dart create mode 100644 example/lib/bar_chart/stacked_target_line.dart create mode 100644 example/lib/bar_chart/vertical_bar_label.dart create mode 100644 example/lib/behaviors/behaviors_gallery.dart create mode 100644 example/lib/behaviors/chart_title.dart create mode 100644 example/lib/behaviors/initial_hint_animation.dart create mode 100644 example/lib/behaviors/initial_selection.dart create mode 100644 example/lib/behaviors/percent_of_domain.dart create mode 100644 example/lib/behaviors/percent_of_domain_by_category.dart create mode 100644 example/lib/behaviors/percent_of_series.dart create mode 100644 example/lib/behaviors/selection_bar_highlight.dart create mode 100644 example/lib/behaviors/selection_callback_example.dart create mode 100644 example/lib/behaviors/selection_line_highlight.dart create mode 100644 example/lib/behaviors/selection_line_highlight_custom_shape.dart create mode 100644 example/lib/behaviors/selection_scatter_plot_highlight.dart create mode 100644 example/lib/behaviors/selection_user_managed.dart create mode 100644 example/lib/behaviors/slider.dart create mode 100644 example/lib/behaviors/sliding_viewport_on_selection.dart create mode 100644 example/lib/combo_chart/combo_gallery.dart create mode 100644 example/lib/combo_chart/date_time_line_point.dart create mode 100644 example/lib/combo_chart/numeric_line_bar.dart create mode 100644 example/lib/combo_chart/numeric_line_point.dart create mode 100644 example/lib/combo_chart/ordinal_bar_line.dart create mode 100644 example/lib/combo_chart/scatter_plot_line.dart create mode 100644 example/lib/drawer.dart create mode 100644 example/lib/gallery_scaffold.dart create mode 100644 example/lib/home.dart create mode 100644 example/lib/i18n/i18n_gallery.dart create mode 100644 example/lib/i18n/rtl_bar_chart.dart create mode 100644 example/lib/i18n/rtl_line_chart.dart create mode 100644 example/lib/i18n/rtl_line_segments.dart create mode 100644 example/lib/i18n/rtl_series_legend.dart create mode 100644 example/lib/legends/datum_legend_options.dart create mode 100644 example/lib/legends/datum_legend_with_measures.dart create mode 100644 example/lib/legends/default_hidden_series_legend.dart create mode 100644 example/lib/legends/legend_custom_symbol.dart create mode 100644 example/lib/legends/legends_gallery.dart create mode 100644 example/lib/legends/series_legend_options.dart create mode 100644 example/lib/legends/series_legend_with_measures.dart create mode 100644 example/lib/legends/simple_datum_legend.dart create mode 100644 example/lib/legends/simple_series_legend.dart create mode 100644 example/lib/line_chart/animation_zoom.dart create mode 100644 example/lib/line_chart/area_and_line.dart create mode 100644 example/lib/line_chart/dash_pattern.dart create mode 100644 example/lib/line_chart/line_annotation.dart create mode 100644 example/lib/line_chart/line_gallery.dart create mode 100644 example/lib/line_chart/points.dart create mode 100644 example/lib/line_chart/range_annotation.dart create mode 100644 example/lib/line_chart/range_annotation_margin.dart create mode 100644 example/lib/line_chart/segments.dart create mode 100644 example/lib/line_chart/simple.dart create mode 100644 example/lib/line_chart/simple_nulls.dart create mode 100644 example/lib/line_chart/stacked_area.dart create mode 100644 example/lib/line_chart/stacked_area_custom_color.dart create mode 100644 example/lib/line_chart/stacked_area_nulls.dart create mode 100644 example/lib/main.dart create mode 100644 example/lib/pie_chart/auto_label.dart create mode 100644 example/lib/pie_chart/donut.dart create mode 100644 example/lib/pie_chart/gauge.dart create mode 100644 example/lib/pie_chart/outside_label.dart create mode 100644 example/lib/pie_chart/partial_pie.dart create mode 100644 example/lib/pie_chart/pie_gallery.dart create mode 100644 example/lib/pie_chart/simple.dart create mode 100644 example/lib/scatter_plot_chart/animation_zoom.dart create mode 100644 example/lib/scatter_plot_chart/bucketing_axis.dart create mode 100644 example/lib/scatter_plot_chart/comparison_points.dart create mode 100644 example/lib/scatter_plot_chart/scatter_plot_gallery.dart create mode 100644 example/lib/scatter_plot_chart/shapes.dart create mode 100644 example/lib/scatter_plot_chart/simple.dart create mode 100644 example/lib/time_series_chart/confidence_interval.dart create mode 100644 example/lib/time_series_chart/end_points_axis.dart create mode 100644 example/lib/time_series_chart/line_annotation.dart create mode 100644 example/lib/time_series_chart/range_annotation.dart create mode 100644 example/lib/time_series_chart/range_annotation_margin.dart create mode 100644 example/lib/time_series_chart/simple.dart create mode 100644 example/lib/time_series_chart/symbol_annotation.dart create mode 100644 example/lib/time_series_chart/time_series_gallery.dart create mode 100644 example/lib/time_series_chart/with_bar_renderer.dart create mode 100644 example/pubspec.yaml create mode 100644 lib/flutter.dart create mode 100644 lib/src/bar_chart.dart create mode 100644 lib/src/base_chart.dart create mode 100644 lib/src/base_chart_state.dart create mode 100644 lib/src/behaviors/a11y/domain_a11y_explore_behavior.dart create mode 100644 lib/src/behaviors/calculation/percent_injector.dart create mode 100644 lib/src/behaviors/chart_behavior.dart create mode 100644 lib/src/behaviors/chart_title/chart_title.dart create mode 100644 lib/src/behaviors/domain_highlighter.dart create mode 100644 lib/src/behaviors/initial_selection.dart create mode 100644 lib/src/behaviors/legend/datum_legend.dart create mode 100644 lib/src/behaviors/legend/legend.dart create mode 100644 lib/src/behaviors/legend/legend_content_builder.dart create mode 100644 lib/src/behaviors/legend/legend_entry_layout.dart create mode 100644 lib/src/behaviors/legend/legend_layout.dart create mode 100644 lib/src/behaviors/legend/series_legend.dart create mode 100644 lib/src/behaviors/line_point_highlighter.dart create mode 100644 lib/src/behaviors/range_annotation.dart create mode 100644 lib/src/behaviors/select_nearest.dart create mode 100644 lib/src/behaviors/slider/slider.dart create mode 100644 lib/src/behaviors/sliding_viewport.dart create mode 100644 lib/src/behaviors/zoom/initial_hint_behavior.dart create mode 100644 lib/src/behaviors/zoom/pan_and_zoom_behavior.dart create mode 100644 lib/src/behaviors/zoom/pan_behavior.dart create mode 100644 lib/src/canvas/circle_sector_painter.dart create mode 100644 lib/src/canvas/line_painter.dart create mode 100644 lib/src/canvas/pie_painter.dart create mode 100644 lib/src/canvas/point_painter.dart create mode 100644 lib/src/canvas/polygon_painter.dart create mode 100644 lib/src/cartesian_chart.dart create mode 100644 lib/src/chart_canvas.dart create mode 100644 lib/src/chart_container.dart create mode 100644 lib/src/chart_gesture_detector.dart create mode 100644 lib/src/chart_state.dart create mode 100644 lib/src/combo_chart/combo_chart.dart create mode 100644 lib/src/graphics_factory.dart create mode 100644 lib/src/line_chart.dart create mode 100644 lib/src/line_style.dart create mode 100644 lib/src/pie_chart.dart create mode 100644 lib/src/scatter_plot_chart.dart create mode 100644 lib/src/selection_model_config.dart create mode 100644 lib/src/symbol_renderer.dart create mode 100644 lib/src/text_element.dart create mode 100644 lib/src/text_style.dart create mode 100644 lib/src/time_series_chart.dart create mode 100644 lib/src/user_managed_state.dart create mode 100644 lib/src/util.dart create mode 100644 lib/src/util/color.dart create mode 100644 lib/src/widget_layout_delegate.dart create mode 100644 minimum_os.bzl create mode 100644 pubspec.yaml create mode 100644 test/behaviors/legend/legend_layout_test.dart create mode 100644 test/text_element_test.dart create mode 100644 test/user_managed_state_test.dart create mode 100644 test/widget_layout_delegate_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4ad8801 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# 0.12.0 +* Add functions to draw horizontal or vertical links onto the dart charts web canvas. +* Add "preserveSelection" functionality to InitialSelection chart Behavior. +* Bug fixes. + +# 0.11.0 +* Null support +* Update to latest from internal repo + +# 0.10.0 +* Internal bug fixes +* Bump versions of intl due to pull request + +# 0.9.0 +* Internal bug fixes +* Bump versions in Gemlock file due to security alerts + +# 0.8.1 +* Update intl version. + +# 0.8.0 +* Bug fixes from open source. + +# 0.7.0 +* Added vertical bar label + +# 0.6.0 +* Bars can now be rendered on line charts. +* Negative measure values will now be rendered on bar charts as a separate stack from the positive +values. +* Added a Datum Legend, which displays one entry per value in the first series on the chart. This is + useful for pie and scatter plot charts. +* The AxisPosition enum in RTLSpec was refactored to AxisDirection to better reflect its effect on +swapping the positions of all start and end components, and not just positioning the measure axes. +* Added custom colors for line renderer area skirts and confidence intervals. A new "areaColorFn" +has been added to Series, and corresponding data to the datum. We could not use the fillColorFn for +these elements, because that color is already applied to the internal section of points on line +charts (including highlighter behaviors). + +# 0.5.0 +* SelectionModelConfig's listener parameter has been renamed to "changeListener". This is a breaking +change. Please rename any existing uses of the "listener" parameter to "changeListener". This was +named in order to add an additional listener "updateListener" that listens to any update requests, +regardless if the selection model has changed. +* CartesianChart's method getMeasureAxis(String axisId) has been changed to +getMeasureAxis({String axisId) so that getting the primary measure axis will not need passing any id +that does not match the secondary measure axis id. This affects users implementing custom behaviors +using the existing method. + +# 0.4.0 +* Fixed export file to export ChartsBehavior in the Flutter library instead of the one that resides +in charts_common. The charts_common behavior should not be used except internally in the +charts_flutter library. This is a breaking change if you are using charts_common behavior. +* Declare compatibility with Dart 2. +* BasicNumericTickFormatterSpec now takes in a callback instead of NumberFormat as the default +constructor. Use named constructor withNumberFormat instead. This is a breaking change. +* BarRendererConfig is no longer default of type String, please change current usage to +BarRendererConfig. This is a breaking change. +* BarTargetLineRendererConfig is no longer default of type String, please change current usage to +BarTargetLineRendererConfig. 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()'. +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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/charts_flutter.gwsq b/charts_flutter.gwsq new file mode 100644 index 0000000..56a60ed --- /dev/null +++ b/charts_flutter.gwsq @@ -0,0 +1,6 @@ +send_cls_to('dart-charts-team+reviews'); +send_cls_to('dart-charts-team'); + +define Main { + reassign_to_list(from_owners_file('third_party/dart/charts_common/OWNERS')); +} diff --git a/example/android.iml b/example/android.iml new file mode 100644 index 0000000..462b903 --- /dev/null +++ b/example/android.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/example/android/Android_Charts.xml b/example/android/Android_Charts.xml new file mode 100644 index 0000000..fd24220 --- /dev/null +++ b/example/android/Android_Charts.xml @@ -0,0 +1,7 @@ + + diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..ca84ef1 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,51 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.examples" + minSdkVersion 16 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9144147 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/java/com/example/examples/MainActivity.java b/example/android/app/src/main/java/com/example/examples/MainActivity.java new file mode 100644 index 0000000..89a83b2 --- /dev/null +++ b/example/android/app/src/main/java/com/example/examples/MainActivity.java @@ -0,0 +1,17 @@ +package com.example.examples; + +import android.os.Bundle; + +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +/** + * FlutterActivity + */ +public class MainActivity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..6de3728 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.3' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..38c8d45 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.jar b/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..6bb8b52 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Feb 19 15:38:12 AWST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/example/android/gradlew b/example/android/gradlew new file mode 100755 index 0000000..9d82f78 --- /dev/null +++ b/example/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/example/android/gradlew.bat b/example/android/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/example/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/example/examples.iml b/example/examples.iml new file mode 100644 index 0000000..4881df8 --- /dev/null +++ b/example/examples.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/example/examples_android.iml b/example/examples_android.iml new file mode 100644 index 0000000..0ca70ed --- /dev/null +++ b/example/examples_android.iml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..6c2de80 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 8.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/example/ios/Ios_Charts.xml b/example/ios/Ios_Charts.xml new file mode 100644 index 0000000..29fdd55 --- /dev/null +++ b/example/ios/Ios_Charts.xml @@ -0,0 +1,7 @@ + + diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0c51fe5 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,436 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.examples; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.examples; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..89550f6 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h new file mode 100644 index 0000000..3fbf7a5 --- /dev/null +++ b/example/ios/Runner/AppDelegate.h @@ -0,0 +1,22 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +@interface AppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m new file mode 100644 index 0000000..b87ca49 --- /dev/null +++ b/example/ios/Runner/AppDelegate.m @@ -0,0 +1,58 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "AppDelegate.h" +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for + // certain types of temporary interruptions (such as an incoming phone call or SMS message) or + // when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame + // rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store + // enough application state information to restore your application to its current state in case + // it is terminated later. + // If your application supports background execution, this method is called instead of + // applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo + // many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. + // If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also + // applicationDidEnterBackground:. +} + +@end diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..033a072 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,82 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-Small@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-Small-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-Small-40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-Small@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-Small-40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-83.5@2x.png", + "scale" : "2x" + } + ] +} \ No newline at end of file diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c767e73aefff48ba192e1d2f59204edc5ccadcb1 GIT binary patch literal 4977 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 LNkvXXu0mjfbW9F# literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png new file mode 100644 index 0000000000000000000000000000000000000000..de0a6dfbc7765ed3b87a55473ba6eb01dd597cab GIT binary patch literal 3279 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40.png new file mode 100644 index 0000000000000000000000000000000000000000..31693a3d9fec13c764bbc6b8d63d1586c24829bc GIT binary patch literal 1769 zcmVPx*qe(^u#PDi_ zR}&K@sSg@WeDm^5Vu-Y0qKSzO zTgW@_Qg>5Vn5PB)Npk4h4}h-W1QIMy4U*MglD~_iE*dKw)WpogfB1Jl^%Fu4>y9i# zEBksZxINTG)PhB3=^h2XxgJNyFcEYkO9U*q2n$VBJ{rsAJ?1rJn7#Tzre;EV29P(l zQg>4y8t&?S;1J>3`xBVRVLe$mYT#VF_*fK6ia7GLh(Rpps0Un<%Fo`U)=~==j)`u< zwNAomYGZ^DNV>YLS4x9DR};a`l`*WLhs%;#B9>iZQ~fQ}M(WIJyctH);ZD$Uf^!3U zljUL@aU+M_S{FrqSnls)@yN`7)UT@&~gDi7@y;VrYaZP>O#S(7p0nh0eUP2ma*3-&UM5c9>5xlzGMO9HytrS5ZU`?^ZMLlAyk-Z5p zaO_X1#73)yljT}yIl*ZPb6LKG4K`Ot*xp5Lj;T%I<*=aka;p|NoVpah_IUOyMw?LU zb1RsH1t(v+Fr0!rOE|qm(4@(7e0OXj+z##smz7wI+}A)0s=5YV#oexjY-gI0S!Teo zUdKHjpLKaSJ(xf>EmB50SukA$t(n|JpEGc!=}&W)a{{VK1|)wJIX+(Je$Fmm8nlCf zAh_S}q;@sjzadiUpg0Xu^4YN4bb<%=XTw}B7Wd#`gBiG z`nlkGQ*d_QRzaGjbTmO~TBqS+SWY;-cv#Vfbxy$S&OEm0d$?55MJrQ-_0$s@{<}*2Ko?#}U-#X*mlez`QM}CQ)j( z)oiD}>%#ZqxOu({=Z~GhVBe5^l<(D?#i)@WL%T*jr*6-a`o@amXIYUBX<-Ei&%?;Y zPV}Do3*S#9;SzS=*7bP0PTyvs#TZUeEI+pX9jdkRWJ86EJq=OwRzFQnb<|jH9QQiT zpy%W%#3vj~xG{X!c9r!Uyhq4q>U3}xTFggNKdbAzc-EAY-KBg(Yv|jA)T{o|=VRi* z7)H*v;m+|tXjNw6vPw9F9`%y=w(Sb*`FP(zhO(sAh5?OpjLJic{6&o$3Awi+h8Gt5 z7`yN{ZXfv-36B#}Dmb|TZAX&}QkMMi@F|?TF%V`jF&)(yUsH8HSCGhY8x}fvYjqM$ z?g(NFYT=hJgA>U-5wei7q=CFXC!}WfV5GUZ`4$V$Q=g!Q4iwg%5;dwWi=uHw6>bfT z<5J%+5{w){r3;VK;BlXI>GlwMhsI3M)Kmm6EApD-)K6VD66pczO=`iyj<9y+V(i<# z9PPaIhVxuiQmEp>-)k#PXHc%qwy+k+U>YANl|@ z$%W}mcRxNld=l*!dd!l@erJeJ&=S7HOB+_>o!8c4Wlg!Qqonx-)f`|>%>g0j?+BQK zU+fvepIvt_!Lp_{NRoV3RuseLhNXDi0Q$6?v!8W8W*c(^oY9hyN;Qx65Z$C-gX@!9 zspLM5-p_I;%W}2gLv+(*@()2D{l6i6pHbu}dnnDdP=BP=W`y>?QD(rv<#e?000000 LNkvXXu0mjfT2@r2 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fec316a13e2159a9131203c23293aa9aa1d4a3c3 GIT binary patch literal 3417 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c767e73aefff48ba192e1d2f59204edc5ccadcb1 GIT binary patch literal 4977 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 IM6N<$f<+)}djJ3c literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..426123bcfb95b580a37a1e8d21e2e5c2b67f4327 GIT binary patch literal 2528 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 Ef-=nmZvX%Q literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..ebf48f6 --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..677c2dc --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Charts Flutter Gallery + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m new file mode 100644 index 0000000..8759ed8 --- /dev/null +++ b/example/ios/Runner/main.m @@ -0,0 +1,24 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/example/lib/a11y/a11y_gallery.dart b/example/lib/a11y/a11y_gallery.dart new file mode 100644 index 0000000..c5d4b5f --- /dev/null +++ b/example/lib/a11y/a11y_gallery.dart @@ -0,0 +1,29 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'domain_a11y_explore_bar_chart.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.accessibility), + title: 'Screen reader enabled bar chart', + subtitle: 'Requires TalkBack or Voiceover turned on to work. ' + 'Bar chart with domain selection explore mode behavior.', + childBuilder: () => new DomainA11yExploreBarChart.withRandomData(), + ), + ]; +} diff --git a/example/lib/a11y/domain_a11y_explore_bar_chart.dart b/example/lib/a11y/domain_a11y_explore_bar_chart.dart new file mode 100644 index 0000000..d02f2e8 --- /dev/null +++ b/example/lib/a11y/domain_a11y_explore_bar_chart.dart @@ -0,0 +1,215 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a bar chart with domain selection A11y behavior. +/// +/// The OS screen reader (TalkBack / VoiceOver) setting must be turned on, or +/// the behavior does not do anything. +/// +/// Note that the screenshot does not show any visual differences but when the +/// OS screen reader is enabled, the node that is being read out loud will be +/// surrounded by a rectangle. +/// +/// When [DomainA11yExploreBehavior] is added to the chart, the chart will +/// listen for the gesture that triggers "explore mode". +/// "Explore mode" creates semantic nodes for each domain value in the chart +/// with a description (customizable, defaults to domain value) and a bounding +/// box that surrounds the domain. +/// +/// These semantic node descriptions are read out loud by the OS screen reader +/// when the user taps within the bounding box, or when the user cycles through +/// the screen's elements (such as swiping left and right). +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class DomainA11yExploreBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DomainA11yExploreBarChart(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory DomainA11yExploreBarChart.withSampleData() { + return new DomainA11yExploreBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DomainA11yExploreBarChart.withRandomData() { + return new DomainA11yExploreBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final mobileData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletData = [ + // Purposely missing data to show that only measures that are available + // are vocalized. + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Mobile Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileData, + ), + new charts.Series( + id: 'Tablet Sales', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletData, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + /// An example of how to generate a customized vocalization for + /// [DomainA11yExploreBehavior] from a list of [SeriesDatum]s. + /// + /// The list of series datums is for one domain. + /// + /// This example vocalizes the domain, then for each series that has that + /// domain, it vocalizes the series display name and the measure and a + /// description of that measure. + String vocalizeDomainAndMeasures(List seriesDatums) { + final buffer = new StringBuffer(); + + // The datum's type in this case is [OrdinalSales]. + // So we can access year and sales information here. + buffer.write(seriesDatums.first.datum.year); + + for (charts.SeriesDatum seriesDatum in seriesDatums) { + final series = seriesDatum.series; + final datum = seriesDatum.datum; + + buffer.write(' ${series.displayName} ' + '${datum.sales / 1000} thousand dollars'); + } + + return buffer.toString(); + } + + @override + Widget build(BuildContext context) { + return new Semantics( + // Describe your chart + label: 'Yearly sales bar chart', + // Optionally provide a hint for the user to know how to trigger + // explore mode. + hint: 'Press and hold to enable explore', + child: new charts.BarChart( + seriesList, + animate: animate, + // To prevent conflict with the select nearest behavior that uses the + // tap gesture, turn off default interactions when the user is using + // an accessibility service like TalkBack or VoiceOver to interact + // with the application. + defaultInteractions: !MediaQuery.of(context).accessibleNavigation, + behaviors: [ + new charts.DomainA11yExploreBehavior( + // Callback for generating the message that is vocalized. + // An example of how to use is in [vocalizeDomainAndMeasures]. + // If none is set, the default only vocalizes the domain value. + vocalizationCallback: vocalizeDomainAndMeasures, + // The following settings are optional, but shown here for + // demonstration purchases. + // [exploreModeTrigger] Default is press and hold, can be + // changed to tap. + exploreModeTrigger: charts.ExploreModeTrigger.pressHold, + // [exploreModeEnabledAnnouncement] Optionally notify the OS + // when explore mode is enabled. + exploreModeEnabledAnnouncement: 'Explore mode enabled', + // [exploreModeDisabledAnnouncement] Optionally notify the OS + // when explore mode is disabled. + exploreModeDisabledAnnouncement: 'Explore mode disabled', + // [minimumWidth] Default and minimum is 1.0. This is the + // minimum width of the screen reader bounding box. The bounding + // box width is calculated based on the domain axis step size. + // Minimum width will be used if the step size is smaller. + minimumWidth: 1.0, + ), + // Optionally include domain highlighter as a behavior. + // This behavior is included in this example to show that when an + // a11y node has focus, the chart's internal selection model is + // also updated. + new charts.DomainHighlighter(charts.SelectionModelType.info), + ], + )); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final mobileData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletData = [ + // Purposely missing data to show that only measures that are available + // are vocalized. + new OrdinalSales('2016', 25), + new OrdinalSales('2017', 50), + ]; + + return [ + new charts.Series( + id: 'Mobile Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileData, + ), + new charts.Series( + id: 'Tablet Sales', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletData, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/app_config.dart b/example/lib/app_config.dart new file mode 100644 index 0000000..a0a91e9 --- /dev/null +++ b/example/lib/app_config.dart @@ -0,0 +1,44 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +/// A particular configuration of the app. +class AppConfig { + final String appName; + final String appLink; + final ThemeData theme; + final bool showPerformanceOverlay; + + AppConfig({ + required this.appName, + required this.appLink, + required this.theme, + required this.showPerformanceOverlay, + }); +} + +/// The default configuration of the app. +AppConfig get defaultConfig { + return new AppConfig( + appName: 'Charts Gallery', + appLink: '', + theme: new ThemeData( + brightness: Brightness.light, + primarySwatch: Colors.lightBlue, + ), + showPerformanceOverlay: false, + ); +} diff --git a/example/lib/axes/axes_gallery.dart b/example/lib/axes/axes_gallery.dart new file mode 100644 index 0000000..f192e2c --- /dev/null +++ b/example/lib/axes/axes_gallery.dart @@ -0,0 +1,137 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'bar_secondary_axis.dart'; +import 'bar_secondary_axis_only.dart'; +import 'custom_axis_tick_formatters.dart'; +import 'custom_font_size_and_color.dart'; +import 'custom_measure_tick_count.dart'; +import 'gridline_dash_pattern.dart'; +import 'hidden_ticks_and_labels_axis.dart'; +import 'horizontal_bar_secondary_axis.dart'; +import 'integer_only_measure_axis.dart'; +import 'line_disjoint_axis.dart'; +import 'measure_axis_label_alignment.dart'; +import 'numeric_initial_viewport.dart'; +import 'nonzero_bound_measure_axis.dart'; +import 'ordinal_initial_viewport.dart'; +import 'short_tick_length_axis.dart'; +import 'statically_provided_ticks.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Bar chart with Secondary Measure Axis', + subtitle: 'Bar chart with a series using a secondary measure axis', + childBuilder: () => new BarChartWithSecondaryAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Bar chart with Secondary Measure Axis only', + subtitle: 'Bar chart with both series using secondary measure axis', + childBuilder: () => new BarChartWithSecondaryAxisOnly.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal bar chart with Secondary Measure Axis', + subtitle: + 'Horizontal Bar chart with a series using secondary measure axis', + childBuilder: () => + new HorizontalBarChartWithSecondaryAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Short Ticks Axis', + subtitle: 'Bar chart with the primary measure axis having short ticks', + childBuilder: () => new ShortTickLengthAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Custom Axis Fonts', + subtitle: 'Bar chart with custom axis font size and color', + childBuilder: () => new CustomFontSizeAndColor.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Label Alignment Axis', + subtitle: 'Bar chart with custom measure axis label alignments', + childBuilder: () => new MeasureAxisLabelAlignment.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'No Axis', + subtitle: 'Bar chart with only the axis line drawn', + childBuilder: () => new HiddenTicksAndLabelsAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Statically Provided Ticks', + subtitle: 'Bar chart with statically provided ticks', + childBuilder: () => new StaticallyProvidedTicks.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Custom Formatter', + subtitle: 'Timeseries with custom domain and measure tick formatters', + childBuilder: () => new CustomAxisTickFormatters.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Custom Tick Count', + subtitle: 'Timeseries with custom measure axis tick count', + childBuilder: () => new CustomMeasureTickCount.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Integer Measure Ticks', + subtitle: 'Timeseries with only whole number measure axis ticks', + childBuilder: () => new IntegerOnlyMeasureAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Non-zero bound Axis', + subtitle: 'Timeseries with measure axis that does not include zero', + childBuilder: () => new NonzeroBoundMeasureAxis.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Ordinal axis with initial viewport', + subtitle: 'Single series with initial viewport', + childBuilder: () => new OrdinalInitialViewport.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Numeric axis with initial viewport', + subtitle: 'Initial viewport is set to a subset of the data', + childBuilder: () => new NumericInitialViewport.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Gridline dash pattern', + subtitle: 'Timeseries with measure gridlines that have a dash pattern', + childBuilder: () => new GridlineDashPattern.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Disjoint Measure Axes', + subtitle: 'Line chart with disjoint measure axes', + childBuilder: () => new DisjointMeasureAxisLineChart.withRandomData(), + ), + ]; +} diff --git a/example/lib/axes/bar_secondary_axis.dart b/example/lib/axes/bar_secondary_axis.dart new file mode 100644 index 0000000..085acd2 --- /dev/null +++ b/example/lib/axes/bar_secondary_axis.dart @@ -0,0 +1,158 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a primary and secondary axis (left & right respectively) +/// for a set of grouped bars. This is useful for comparing Series that have +/// different units (revenue vs clicks by region), or different magnitudes (2017 +/// revenue vs 1/1/2017 revenue by region). +/// +/// The first series plots using the primary axis to position its measure +/// values (bar height). This is the default axis used if the measureAxisId is +/// not set. +/// +/// The second series plots using the secondary axis due to the measureAxisId of +/// secondaryMeasureAxisId. +/// +/// Note: primary and secondary may flip left and right positioning when +/// RTL.flipAxisLocations is set. +class BarChartWithSecondaryAxis extends StatelessWidget { + static const secondaryMeasureAxisId = 'secondaryMeasureAxisId'; + final List> seriesList; + final bool animate; + + BarChartWithSecondaryAxis(this.seriesList, {this.animate = false}); + + factory BarChartWithSecondaryAxis.withSampleData() { + return new BarChartWithSecondaryAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory BarChartWithSecondaryAxis.withRandomData() { + return new BarChartWithSecondaryAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + final losAngelesSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + new charts.Series( + id: 'Los Angeles Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: losAngelesSalesData, + )..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId) + // Set the 'Los Angeles Revenue' series to use the secondary measure axis. + // All series that have this set will use the secondary measure axis. + // All other series will use the primary measure axis. + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // It is important when using both primary and secondary axes to choose + // the same number of ticks for both sides to get the gridlines to line + // up. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 3)), + secondaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 3)), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + final losAngelesSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + new charts.Series( + id: 'Los Angeles Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: losAngelesSalesData, + )..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId) + // Set the 'Los Angeles Revenue' series to use the secondary measure axis. + // All series that have this set will use the secondary measure axis. + // All other series will use the primary measure axis. + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/axes/bar_secondary_axis_only.dart b/example/lib/axes/bar_secondary_axis_only.dart new file mode 100644 index 0000000..b205b1d --- /dev/null +++ b/example/lib/axes/bar_secondary_axis_only.dart @@ -0,0 +1,114 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using only a secondary axis (on the right) for a set of grouped +/// bars. +/// +/// Both series plots using the secondary axis due to the measureAxisId of +/// secondaryMeasureAxisId. +/// +/// Note: secondary may flip left and right positioning when +/// RTL.flipAxisLocations is set. +class BarChartWithSecondaryAxisOnly extends StatelessWidget { + static const secondaryMeasureAxisId = 'secondaryMeasureAxisId'; + final List> seriesList; + final bool animate; + + BarChartWithSecondaryAxisOnly(this.seriesList, {this.animate = false}); + + factory BarChartWithSecondaryAxisOnly.withSampleData() { + return new BarChartWithSecondaryAxisOnly( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory BarChartWithSecondaryAxisOnly.withRandomData() { + return new BarChartWithSecondaryAxisOnly(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ) + // Set series to use the secondary measure axis. + ..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 500), + new OrdinalSales('2015', 2500), + new OrdinalSales('2016', 1000), + new OrdinalSales('2017', 7500), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ) + // Set series to use the secondary measure axis. + ..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/axes/custom_axis_tick_formatters.dart b/example/lib/axes/custom_axis_tick_formatters.dart new file mode 100644 index 0000000..285a17c --- /dev/null +++ b/example/lib/axes/custom_axis_tick_formatters.dart @@ -0,0 +1,144 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of timeseries chart with custom measure and domain formatters. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class CustomAxisTickFormatters extends StatelessWidget { + final List> seriesList; + final bool animate; + + CustomAxisTickFormatters(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory CustomAxisTickFormatters.withSampleData() { + return new CustomAxisTickFormatters( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory CustomAxisTickFormatters.withRandomData() { + return new CustomAxisTickFormatters(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 26), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 27), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 28), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 29), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 30), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 01), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 02), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 03), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 04), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 05), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + /// Formatter for numeric ticks using [NumberFormat] to format into currency + /// + /// This is what is used in the [NumericAxisSpec] below. + final simpleCurrencyFormatter = + new charts.BasicNumericTickFormatterSpec.fromNumberFormat( + new NumberFormat.compactSimpleCurrency()); + + /// Formatter for numeric ticks that uses the callback provided. + /// + /// Use this formatter if you need to format values that [NumberFormat] + /// cannot provide. + /// + /// To see this formatter, change [NumericAxisSpec] to use this formatter. + // final customTickFormatter = + // charts.BasicNumericTickFormatterSpec((num value) => 'MyValue: $value'); + + return new charts.TimeSeriesChart(seriesList, + animate: animate, + // Sets up a currency formatter for the measure axis. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickFormatterSpec: simpleCurrencyFormatter), + + /// Customizes the date tick formatter. It will print the day of month + /// as the default format, but include the month and year if it + /// transitions to a new month. + /// + /// minute, hour, day, month, and year are all provided by default and + /// you can override them following this pattern. + domainAxis: new charts.DateTimeAxisSpec( + tickFormatterSpec: new charts.AutoDateTimeTickFormatterSpec( + day: new charts.TimeFormatterSpec( + format: 'd', transitionFormat: 'MM/dd/yyyy')))); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 6), + new MyRow(new DateTime(2017, 9, 26), 8), + new MyRow(new DateTime(2017, 9, 27), 6), + new MyRow(new DateTime(2017, 9, 28), 9), + new MyRow(new DateTime(2017, 9, 29), 11), + new MyRow(new DateTime(2017, 9, 30), 15), + new MyRow(new DateTime(2017, 10, 01), 25), + new MyRow(new DateTime(2017, 10, 02), 33), + new MyRow(new DateTime(2017, 10, 03), 27), + new MyRow(new DateTime(2017, 10, 04), 31), + new MyRow(new DateTime(2017, 10, 05), 23), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int cost; + MyRow(this.timeStamp, this.cost); +} diff --git a/example/lib/axes/custom_font_size_and_color.dart b/example/lib/axes/custom_font_size_and_color.dart new file mode 100644 index 0000000..e63d192 --- /dev/null +++ b/example/lib/axes/custom_font_size_and_color.dart @@ -0,0 +1,136 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Custom Font Style Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a custom primary measure and domain axis replacing the +/// renderSpec with one with a custom font size and a custom color. +/// +/// There are many axis styling options in the SmallTickRenderer allowing you +/// to customize the font, tick lengths, and offsets. +class CustomFontSizeAndColor extends StatelessWidget { + final List> seriesList; + final bool animate; + + CustomFontSizeAndColor(this.seriesList, {this.animate = false}); + + factory CustomFontSizeAndColor.withSampleData() { + return new CustomFontSizeAndColor( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory CustomFontSizeAndColor.withRandomData() { + return new CustomFontSizeAndColor(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Assign a custom style for the domain axis. + /// + /// This is an OrdinalAxisSpec to match up with BarChart's default + /// ordinal domain axis (use NumericAxisSpec or DateTimeAxisSpec for + /// other charts). + domainAxis: new charts.OrdinalAxisSpec( + renderSpec: new charts.SmallTickRendererSpec( + + // Tick and Label styling here. + labelStyle: new charts.TextStyleSpec( + fontSize: 18, // size in Pts. + color: charts.MaterialPalette.black), + + // Change the line colors to match text color. + lineStyle: new charts.LineStyleSpec( + color: charts.MaterialPalette.black))), + + /// Assign a custom style for the measure axis. + primaryMeasureAxis: new charts.NumericAxisSpec( + renderSpec: new charts.GridlineRendererSpec( + + // Tick and Label styling here. + labelStyle: new charts.TextStyleSpec( + fontSize: 18, // size in Pts. + color: charts.MaterialPalette.black), + + // Change the line colors to match text color. + lineStyle: new charts.LineStyleSpec( + color: charts.MaterialPalette.black))), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/axes/custom_measure_tick_count.dart b/example/lib/axes/custom_measure_tick_count.dart new file mode 100644 index 0000000..dfa7bdd --- /dev/null +++ b/example/lib/axes/custom_measure_tick_count.dart @@ -0,0 +1,122 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of timeseries chart with a custom number of ticks +/// +/// The tick count can be set by setting the [desiredMinTickCount] and +/// [desiredMaxTickCount] for automatically adjusted tick counts (based on +/// how 'nice' the ticks are) or [desiredTickCount] for a fixed tick count. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class CustomMeasureTickCount extends StatelessWidget { + final List> seriesList; + final bool animate; + + CustomMeasureTickCount(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory CustomMeasureTickCount.withSampleData() { + return new CustomMeasureTickCount( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory CustomMeasureTickCount.withRandomData() { + return new CustomMeasureTickCount(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 26), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 27), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 28), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 29), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 30), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 01), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 02), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 03), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 04), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 05), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, + animate: animate, + + /// Customize the measure axis to have 2 ticks, + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 2))); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 6), + new MyRow(new DateTime(2017, 9, 26), 8), + new MyRow(new DateTime(2017, 9, 27), 6), + new MyRow(new DateTime(2017, 9, 28), 9), + new MyRow(new DateTime(2017, 9, 29), 11), + new MyRow(new DateTime(2017, 9, 30), 15), + new MyRow(new DateTime(2017, 10, 01), 25), + new MyRow(new DateTime(2017, 10, 02), 33), + new MyRow(new DateTime(2017, 10, 03), 27), + new MyRow(new DateTime(2017, 10, 04), 31), + new MyRow(new DateTime(2017, 10, 05), 23), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int cost; + MyRow(this.timeStamp, this.cost); +} diff --git a/example/lib/axes/flipped_vertical_axis.dart b/example/lib/axes/flipped_vertical_axis.dart new file mode 100644 index 0000000..4c11d14 --- /dev/null +++ b/example/lib/axes/flipped_vertical_axis.dart @@ -0,0 +1,114 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of flipping the vertical measure axis direction so that larger +/// values render downward instead of the usual rendering up. +/// +/// flipVerticalAxis, when set, flips the vertical axis from its default +/// direction. +/// +/// Note: primary and secondary may flip left and right positioning when +/// RTL.flipAxisLocations is set. +class FlippedVerticalAxis extends StatelessWidget { + final List> seriesList; + final bool animate; + + FlippedVerticalAxis(this.seriesList, {this.animate = false}); + + factory FlippedVerticalAxis.withSampleData() { + return new FlippedVerticalAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory FlippedVerticalAxis.withRandomData() { + return new FlippedVerticalAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + const runners = ['Smith', 'Jones', 'Brown', 'Doe']; + + // Randomly assign runners, but leave the order of the places. + final raceData = [ + new RunnerRank(runners.removeAt(random.nextInt(runners.length)), 1), + new RunnerRank(runners.removeAt(random.nextInt(runners.length)), 2), + new RunnerRank(runners.removeAt(random.nextInt(runners.length)), 3), + new RunnerRank(runners.removeAt(random.nextInt(runners.length)), 4), + ]; + + return [ + new charts.Series( + id: 'Race Results', + domainFn: (RunnerRank row, _) => row.name, + measureFn: (RunnerRank row, _) => row.place, + data: raceData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // Known Issue, the bar chart cannot render negative direction bars at this + // time so the result is an empty chart. + // TODO: Remove this comment + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + flipVerticalAxis: true, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final raceData = [ + new RunnerRank('Smith', 1), + new RunnerRank('Jones', 2), + new RunnerRank('Brown', 3), + new RunnerRank('Doe', 4), + ]; + + return [ + new charts.Series( + id: 'Race Results', + domainFn: (RunnerRank row, _) => row.name, + measureFn: (RunnerRank row, _) => row.place, + data: raceData), + ]; + } +} + +/// Datum/Row for the chart. +class RunnerRank { + final String name; + final int place; + RunnerRank(this.name, this.place); +} diff --git a/example/lib/axes/gridline_dash_pattern.dart b/example/lib/axes/gridline_dash_pattern.dart new file mode 100644 index 0000000..1190cb5 --- /dev/null +++ b/example/lib/axes/gridline_dash_pattern.dart @@ -0,0 +1,120 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of timeseries chart with gridlines that have a dash pattern. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class GridlineDashPattern extends StatelessWidget { + final List> seriesList; + final bool animate; + + GridlineDashPattern(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory GridlineDashPattern.withSampleData() { + return new GridlineDashPattern( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GridlineDashPattern.withRandomData() { + return new GridlineDashPattern(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 26), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 27), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 28), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 29), random.nextInt(100)), + new MyRow(new DateTime(2017, 9, 30), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 01), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 02), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 03), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 04), random.nextInt(100)), + new MyRow(new DateTime(2017, 10, 05), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, + animate: animate, + + /// Customize the gridlines to use a dash pattern. + primaryMeasureAxis: new charts.NumericAxisSpec( + renderSpec: charts.GridlineRendererSpec( + lineStyle: charts.LineStyleSpec( + dashPattern: [4, 4], + )))); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 6), + new MyRow(new DateTime(2017, 9, 26), 8), + new MyRow(new DateTime(2017, 9, 27), 6), + new MyRow(new DateTime(2017, 9, 28), 9), + new MyRow(new DateTime(2017, 9, 29), 11), + new MyRow(new DateTime(2017, 9, 30), 15), + new MyRow(new DateTime(2017, 10, 01), 25), + new MyRow(new DateTime(2017, 10, 02), 33), + new MyRow(new DateTime(2017, 10, 03), 27), + new MyRow(new DateTime(2017, 10, 04), 31), + new MyRow(new DateTime(2017, 10, 05), 23), + ]; + + return [ + new charts.Series( + id: 'Cost', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.cost, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int cost; + MyRow(this.timeStamp, this.cost); +} diff --git a/example/lib/axes/hidden_ticks_and_labels_axis.dart b/example/lib/axes/hidden_ticks_and_labels_axis.dart new file mode 100644 index 0000000..a268207 --- /dev/null +++ b/example/lib/axes/hidden_ticks_and_labels_axis.dart @@ -0,0 +1,118 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// No Axis Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of hiding both axis. +class HiddenTicksAndLabelsAxis extends StatelessWidget { + final List> seriesList; + final bool animate; + + HiddenTicksAndLabelsAxis(this.seriesList, {this.animate = false}); + + factory HiddenTicksAndLabelsAxis.withSampleData() { + return new HiddenTicksAndLabelsAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HiddenTicksAndLabelsAxis.withRandomData() { + return new HiddenTicksAndLabelsAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Assign a custom style for the measure axis. + /// + /// The NoneRenderSpec can still draw an axis line with + /// showAxisLine=true. + primaryMeasureAxis: + new charts.NumericAxisSpec(renderSpec: new charts.NoneRenderSpec()), + + /// This is an OrdinalAxisSpec to match up with BarChart's default + /// ordinal domain axis (use NumericAxisSpec or DateTimeAxisSpec for + /// other charts). + domainAxis: new charts.OrdinalAxisSpec( + // Make sure that we draw the domain axis line. + showAxisLine: true, + // But don't draw anything else. + renderSpec: new charts.NoneRenderSpec()), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/axes/horizontal_bar_secondary_axis.dart b/example/lib/axes/horizontal_bar_secondary_axis.dart new file mode 100644 index 0000000..a1c3be2 --- /dev/null +++ b/example/lib/axes/horizontal_bar_secondary_axis.dart @@ -0,0 +1,160 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a primary and secondary axis (left & right respectively) +/// for a set of grouped bars. This is useful for comparing Series that have +/// different units (revenue vs clicks by region), or different magnitudes (2017 +/// revenue vs 1/1/2017 revenue by region). +/// +/// The first series plots using the primary axis to position its measure +/// values (bar height). This is the default axis used if the measureAxisId is +/// not set. +/// +/// The second series plots using the secondary axis due to the measureAxisId of +/// secondaryMeasureAxisId. +/// +/// Note: primary and secondary may flip left and right positioning when +/// RTL.flipAxisLocations is set. +class HorizontalBarChartWithSecondaryAxis extends StatelessWidget { + static const secondaryMeasureAxisId = 'secondaryMeasureAxisId'; + final List> seriesList; + final bool animate; + + HorizontalBarChartWithSecondaryAxis(this.seriesList, {this.animate = false}); + + factory HorizontalBarChartWithSecondaryAxis.withSampleData() { + return new HorizontalBarChartWithSecondaryAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalBarChartWithSecondaryAxis.withRandomData() { + return new HorizontalBarChartWithSecondaryAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + final losAngelesSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + new charts.Series( + id: 'Los Angeles Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: losAngelesSalesData, + )..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId) + // Set the 'Los Angeles Revenue' series to use the secondary measure axis. + // All series that have this set will use the secondary measure axis. + // All other series will use the primary measure axis. + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // For horizontal bar charts, set the [vertical] flag to false. + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + vertical: false, + // It is important when using both primary and secondary axes to choose + // the same number of ticks for both sides to get the gridlines to line + // up. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 3)), + secondaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(desiredTickCount: 3)), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + final losAngelesSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + new charts.Series( + id: 'Los Angeles Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: losAngelesSalesData, + )..setAttribute(charts.measureAxisIdKey, secondaryMeasureAxisId) + // Set the 'Los Angeles Revenue' series to use the secondary measure axis. + // All series that have this set will use the secondary measure axis. + // All other series will use the primary measure axis. + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/axes/integer_only_measure_axis.dart b/example/lib/axes/integer_only_measure_axis.dart new file mode 100644 index 0000000..37e3e24 --- /dev/null +++ b/example/lib/axes/integer_only_measure_axis.dart @@ -0,0 +1,129 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of timeseries chart forcing the measure axis to have whole number +/// ticks. This is useful if the measure units don't make sense to present as +/// fractional. +/// +/// This is done by customizing the measure axis and setting +/// [dataIsInWholeNumbers] on the tick provider. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class IntegerOnlyMeasureAxis extends StatelessWidget { + final List> seriesList; + final bool animate; + + IntegerOnlyMeasureAxis(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory IntegerOnlyMeasureAxis.withSampleData() { + return new IntegerOnlyMeasureAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory IntegerOnlyMeasureAxis.withRandomData() { + return new IntegerOnlyMeasureAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 26), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 27), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 28), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 29), random.nextDouble().round()), + new MyRow(new DateTime(2017, 9, 30), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 01), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 02), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 03), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 04), random.nextDouble().round()), + new MyRow(new DateTime(2017, 10, 05), random.nextDouble().round()), + ]; + + return [ + new charts.Series( + id: 'Headcount', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.headcount, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Provides a custom axis ensuring that the ticks are in whole numbers. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: new charts.BasicNumericTickProviderSpec( + // Make sure we don't have values less than 1 as ticks + // (ie: counts). + dataIsInWholeNumbers: true, + // Fixed tick count to highlight the integer only behavior + // generating ticks [0, 1, 2, 3, 4]. + desiredTickCount: 5)), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 0), + new MyRow(new DateTime(2017, 9, 26), 0), + new MyRow(new DateTime(2017, 9, 27), 0), + new MyRow(new DateTime(2017, 9, 28), 0), + new MyRow(new DateTime(2017, 9, 29), 0), + new MyRow(new DateTime(2017, 9, 30), 0), + new MyRow(new DateTime(2017, 10, 01), 1), + new MyRow(new DateTime(2017, 10, 02), 1), + new MyRow(new DateTime(2017, 10, 03), 1), + new MyRow(new DateTime(2017, 10, 04), 1), + new MyRow(new DateTime(2017, 10, 05), 1), + ]; + + return [ + new charts.Series( + id: 'Headcount', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.headcount, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int headcount; + MyRow(this.timeStamp, this.headcount); +} diff --git a/example/lib/axes/line_disjoint_axis.dart b/example/lib/axes/line_disjoint_axis.dart new file mode 100644 index 0000000..879f151 --- /dev/null +++ b/example/lib/axes/line_disjoint_axis.dart @@ -0,0 +1,268 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of using disjoint measure axes to render 4 series of lines with +/// separate scales. 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 measure axes will be used to scale the series associated with them, +/// but they will not render any tick elements on either side of the chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:collection' show LinkedHashMap; +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class DisjointMeasureAxisLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DisjointMeasureAxisLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory DisjointMeasureAxisLineChart.withSampleData() { + return new DisjointMeasureAxisLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DisjointMeasureAxisLineChart.withRandomData() { + return new DisjointMeasureAxisLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + // The first three series contain similar data with different magnitudes. + // This demonstrates the ability to graph the trends in each series relative + // to each other, without the largest magnitude series compressing the + // smallest. + final myFakeDesktopData = [ + new LinearClicks(0, clickCount: random.nextInt(100)), + new LinearClicks(1, clickCount: random.nextInt(100)), + new LinearClicks(2, clickCount: random.nextInt(100)), + new LinearClicks(3, clickCount: random.nextInt(100)), + ]; + + final myFakeTabletData = [ + new LinearClicks(0, clickCount: random.nextInt(100) * 100), + new LinearClicks(1, clickCount: random.nextInt(100) * 100), + new LinearClicks(2, clickCount: random.nextInt(100) * 100), + new LinearClicks(3, clickCount: random.nextInt(100) * 100), + ]; + + final myFakeMobileData = [ + new LinearClicks(0, clickCount: random.nextInt(100) * 1000), + new LinearClicks(1, clickCount: random.nextInt(100) * 1000), + new LinearClicks(2, clickCount: random.nextInt(100) * 1000), + new LinearClicks(3, clickCount: random.nextInt(100) * 1000), + ]; + + // The fourth series renders with decimal values, representing a very + // different sort ratio-based data. If this was on the same axis as any of + // the other series, it would be squashed near zero. + final myFakeClickRateData = [ + new LinearClicks(0, clickRate: .25), + new LinearClicks(1, clickRate: .65), + new LinearClicks(2, clickRate: .50), + new LinearClicks(3, clickRate: .30), + ]; + + return [ + // We render an empty series on the primary measure axis to ensure that + // the axis itself gets rendered. This helps us draw the gridlines on the + // chart. + new charts.Series( + id: 'Fake Series', + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: []), + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeDesktopData, + ) + // Set the 'Desktop' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 1'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeTabletData, + ) + // Set the 'Tablet' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 2'), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeMobileData, + ) + // Set the 'Mobile' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 3'), + new charts.Series( + id: 'Click Rate', + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeClickRateData, + ) + // Set the 'Click Rate' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 4'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + animate: animate, + // Configure a primary measure axis that will render gridlines across + // the chart. This axis uses fake ticks with no labels to ensure that we + // get 5 grid lines. + // + // We do this because disjoint measure axes do not draw any tick + // elements on the chart. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: new charts.StaticNumericTickProviderSpec( + // Create the ticks to be used the domain axis. + >[ + new charts.TickSpec(0, label: ''), + new charts.TickSpec(1, label: ''), + new charts.TickSpec(2, label: ''), + new charts.TickSpec(3, label: ''), + new charts.TickSpec(4, label: ''), + ], + )), + // Create one disjoint measure axis per series on the chart. + // + // Disjoint measure axes will be used to scale the rendered data, + // without drawing any tick elements on either side of the chart. + disjointMeasureAxes: + new LinkedHashMap.from({ + 'axis 1': new charts.NumericAxisSpec(), + 'axis 2': new charts.NumericAxisSpec(), + 'axis 3': new charts.NumericAxisSpec(), + 'axis 4': new charts.NumericAxisSpec(), + })); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + // The first three series contain similar data with different magnitudes. + // This demonstrates the ability to graph the trends in each series relative + // to each other, without the largest magnitude series compressing the + // smallest. + final myFakeDesktopData = [ + new LinearClicks(0, clickCount: 25), + new LinearClicks(1, clickCount: 125), + new LinearClicks(2, clickCount: 920), + new LinearClicks(3, clickCount: 375), + ]; + + final myFakeTabletData = [ + new LinearClicks(0, clickCount: 375), + new LinearClicks(1, clickCount: 1850), + new LinearClicks(2, clickCount: 9700), + new LinearClicks(3, clickCount: 5000), + ]; + + final myFakeMobileData = [ + new LinearClicks(0, clickCount: 5000), + new LinearClicks(1, clickCount: 25000), + new LinearClicks(2, clickCount: 100000), + new LinearClicks(3, clickCount: 75000), + ]; + + // The fourth series renders with decimal values, representing a very + // different sort ratio-based data. If this was on the same axis as any of + // the other series, it would be squashed near zero. + final myFakeClickRateData = [ + new LinearClicks(0, clickRate: .25), + new LinearClicks(1, clickRate: .65), + new LinearClicks(2, clickRate: .50), + new LinearClicks(3, clickRate: .30), + ]; + + return [ + // We render an empty series on the primary measure axis to ensure that + // the axis itself gets rendered. This helps us draw the gridlines on the + // chart. + new charts.Series( + id: 'Fake Series', + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: []), + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeDesktopData, + ) + // Set the 'Desktop' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 1'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeTabletData, + ) + // Set the 'Tablet' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 2'), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickCount, + data: myFakeMobileData, + ) + // Set the 'Mobile' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 3'), + new charts.Series( + id: 'Click Rate', + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearClicks clickCount, _) => clickCount.year, + measureFn: (LinearClicks clickCount, _) => clickCount.clickRate, + data: myFakeClickRateData, + ) + // Set the 'Click Rate' series to use a disjoint axis. + ..setAttribute(charts.measureAxisIdKey, 'axis 4'), + ]; + } +} + +/// Sample linear data type. +class LinearClicks { + final int year; + final int? clickCount; + final double? clickRate; + + LinearClicks(this.year, {this.clickCount, this.clickRate}); +} diff --git a/example/lib/axes/measure_axis_label_alignment.dart b/example/lib/axes/measure_axis_label_alignment.dart new file mode 100644 index 0000000..fe3251e --- /dev/null +++ b/example/lib/axes/measure_axis_label_alignment.dart @@ -0,0 +1,122 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Custom Tick Label Alignment Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a custom primary measure replacing the renderSpec with one +/// that aligns the text under the tick and left justifies. +class MeasureAxisLabelAlignment extends StatelessWidget { + final List> seriesList; + final bool animate; + + MeasureAxisLabelAlignment(this.seriesList, {this.animate = false}); + + factory MeasureAxisLabelAlignment.withSampleData() { + return new MeasureAxisLabelAlignment( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory MeasureAxisLabelAlignment.withRandomData() { + return new MeasureAxisLabelAlignment(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Customize the primary measure axis using a small tick renderer. + /// Use String instead of num for ordinal domain axis + /// (typically bar charts). + primaryMeasureAxis: new charts.NumericAxisSpec( + renderSpec: new charts.GridlineRendererSpec( + // Display the measure axis labels below the gridline. + // + // 'Before' & 'after' follow the axis value direction. + // Vertical axes draw 'before' below & 'after' above the tick. + // Horizontal axes draw 'before' left & 'after' right the tick. + labelAnchor: charts.TickLabelAnchor.before, + + // Left justify the text in the axis. + // + // Note: outside means that the secondary measure axis would right + // justify. + labelJustification: charts.TickLabelJustification.outside, + )), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/axes/nonzero_bound_measure_axis.dart b/example/lib/axes/nonzero_bound_measure_axis.dart new file mode 100644 index 0000000..da136b7 --- /dev/null +++ b/example/lib/axes/nonzero_bound_measure_axis.dart @@ -0,0 +1,119 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of timeseries chart that has a measure axis that does NOT include +/// zero. It starts at 100 and goes to 140. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class NonzeroBoundMeasureAxis extends StatelessWidget { + final List> seriesList; + final bool animate; + + NonzeroBoundMeasureAxis(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory NonzeroBoundMeasureAxis.withSampleData() { + return new NonzeroBoundMeasureAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory NonzeroBoundMeasureAxis.withRandomData() { + return new NonzeroBoundMeasureAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new MyRow(new DateTime(2017, 9, 25), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 26), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 27), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 28), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 29), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 9, 30), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 01), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 02), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 03), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 04), random.nextInt(100) + 100), + new MyRow(new DateTime(2017, 10, 05), random.nextInt(100) + 100), + ]; + + return [ + new charts.Series( + id: 'Headcount', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.headcount, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, + animate: animate, + // Provide a tickProviderSpec which does NOT require that zero is + // included. + primaryMeasureAxis: new charts.NumericAxisSpec( + tickProviderSpec: + new charts.BasicNumericTickProviderSpec(zeroBound: false))); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new MyRow(new DateTime(2017, 9, 25), 106), + new MyRow(new DateTime(2017, 9, 26), 108), + new MyRow(new DateTime(2017, 9, 27), 106), + new MyRow(new DateTime(2017, 9, 28), 109), + new MyRow(new DateTime(2017, 9, 29), 111), + new MyRow(new DateTime(2017, 9, 30), 115), + new MyRow(new DateTime(2017, 10, 01), 125), + new MyRow(new DateTime(2017, 10, 02), 133), + new MyRow(new DateTime(2017, 10, 03), 127), + new MyRow(new DateTime(2017, 10, 04), 131), + new MyRow(new DateTime(2017, 10, 05), 123), + ]; + + return [ + new charts.Series( + id: 'Headcount', + domainFn: (MyRow row, _) => row.timeStamp, + measureFn: (MyRow row, _) => row.headcount, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class MyRow { + final DateTime timeStamp; + final int headcount; + MyRow(this.timeStamp, this.headcount); +} diff --git a/example/lib/axes/numeric_initial_viewport.dart b/example/lib/axes/numeric_initial_viewport.dart new file mode 100644 index 0000000..6a3826e --- /dev/null +++ b/example/lib/axes/numeric_initial_viewport.dart @@ -0,0 +1,133 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of setting an initial viewport for ordinal axis. +/// +/// This allows for specifying the specific range of data to show that differs +/// from what was provided in the series list. +/// +/// In this example, the series list has numeric data from 0 to 10, but we +/// want to show from 3 to 7. +/// We can do this by specifying an [NumericExtents] in [NumericAxisSpec]. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class NumericInitialViewport extends StatelessWidget { + final List> seriesList; + final bool animate; + + NumericInitialViewport(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory NumericInitialViewport.withSampleData() { + return new NumericInitialViewport( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory NumericInitialViewport.withRandomData() { + return new NumericInitialViewport(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + new LinearSales(7, random.nextInt(100)), + new LinearSales(8, random.nextInt(100)), + new LinearSales(9, random.nextInt(100)), + new LinearSales(10, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart( + seriesList, + animate: animate, + domainAxis: new charts.NumericAxisSpec( + // Set the initial viewport by providing a new AxisSpec with the + // desired viewport, in NumericExtents. + viewport: new charts.NumericExtents(3.0, 7.0)), + // Optionally add a pan or pan and zoom behavior. + // If pan/zoom is not added, the viewport specified remains the viewport. + behaviors: [new charts.PanAndZoomBehavior()], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + new LinearSales(4, 55), + new LinearSales(5, 66), + new LinearSales(6, 110), + new LinearSales(7, 70), + new LinearSales(8, 20), + new LinearSales(9, 25), + new LinearSales(10, 45), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/axes/ordinal_initial_viewport.dart b/example/lib/axes/ordinal_initial_viewport.dart new file mode 100644 index 0000000..157c5fe --- /dev/null +++ b/example/lib/axes/ordinal_initial_viewport.dart @@ -0,0 +1,145 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of setting an initial viewport for ordinal axis. +/// +/// This allows for specifying the specific range of data to show that differs +/// from what was provided in the series list. +/// +/// In this example, the series list has ordinal data from year 2014 to 2030, +/// but we want to show starting at 2018 and we only want to show 4 values. +/// We can do this by specifying an [OrdinalViewport] in [OrdinalAxisSpec]. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class OrdinalInitialViewport extends StatelessWidget { + final List> seriesList; + final bool animate; + + OrdinalInitialViewport(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory OrdinalInitialViewport.withSampleData() { + return new OrdinalInitialViewport( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory OrdinalInitialViewport.withRandomData() { + return new OrdinalInitialViewport(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + new OrdinalSales('2018', random.nextInt(100)), + new OrdinalSales('2019', random.nextInt(100)), + new OrdinalSales('2020', random.nextInt(100)), + new OrdinalSales('2021', random.nextInt(100)), + new OrdinalSales('2022', random.nextInt(100)), + new OrdinalSales('2023', random.nextInt(100)), + new OrdinalSales('2024', random.nextInt(100)), + new OrdinalSales('2025', random.nextInt(100)), + new OrdinalSales('2026', random.nextInt(100)), + new OrdinalSales('2027', random.nextInt(100)), + new OrdinalSales('2028', random.nextInt(100)), + new OrdinalSales('2029', random.nextInt(100)), + new OrdinalSales('2030', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Set the initial viewport by providing a new AxisSpec with the + // desired viewport: a starting domain and the data size. + domainAxis: new charts.OrdinalAxisSpec( + viewport: new charts.OrdinalViewport('2018', 4)), + // Optionally add a pan or pan and zoom behavior. + // If pan/zoom is not added, the viewport specified remains the viewport. + behaviors: [new charts.PanAndZoomBehavior()], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + new OrdinalSales('2018', 33), + new OrdinalSales('2019', 80), + new OrdinalSales('2020', 21), + new OrdinalSales('2021', 77), + new OrdinalSales('2022', 8), + new OrdinalSales('2023', 12), + new OrdinalSales('2024', 42), + new OrdinalSales('2025', 70), + new OrdinalSales('2026', 77), + new OrdinalSales('2027', 55), + new OrdinalSales('2028', 19), + new OrdinalSales('2029', 66), + new OrdinalSales('2030', 27), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/axes/short_tick_length_axis.dart b/example/lib/axes/short_tick_length_axis.dart new file mode 100644 index 0000000..b720736 --- /dev/null +++ b/example/lib/axes/short_tick_length_axis.dart @@ -0,0 +1,115 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Custom Tick Style Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of using a custom primary measure axis replacing the default +/// gridline rendering with a short tick rendering. It also turns on the axis +/// line so that the ticks have something to line up against. +/// +/// There are many axis styling options in the SmallTickRenderer allowing you +/// to customize the font, tick lengths, and offsets. +class ShortTickLengthAxis extends StatelessWidget { + final List> seriesList; + final bool animate; + + ShortTickLengthAxis(this.seriesList, {this.animate = false}); + + factory ShortTickLengthAxis.withSampleData() { + return new ShortTickLengthAxis( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ShortTickLengthAxis.withRandomData() { + return new ShortTickLengthAxis(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Customize the primary measure axis using a small tick renderer. + /// Note: use String instead of num for ordinal domain axis + /// (typically bar charts). + primaryMeasureAxis: new charts.NumericAxisSpec( + renderSpec: new charts.SmallTickRendererSpec( + // Tick and Label styling here. + )), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/axes/statically_provided_ticks.dart b/example/lib/axes/statically_provided_ticks.dart new file mode 100644 index 0000000..5990b7b --- /dev/null +++ b/example/lib/axes/statically_provided_ticks.dart @@ -0,0 +1,134 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of axis using statically provided ticks. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of specifying a custom set of ticks to be used on the domain axis. +/// +/// Specifying custom set of ticks allows specifying exactly what ticks are +/// used in the axis. Each tick is also allowed to have a different style set. +/// +/// For an ordinal axis, the [StaticOrdinalTickProviderSpec] is shown in this +/// example defining ticks to be used with [TickSpec] of String. +/// +/// For numeric axis, the [StaticNumericTickProviderSpec] can be used by passing +/// in a list of ticks defined with [TickSpec] of num. +/// +/// For datetime axis, the [StaticDateTimeTickProviderSpec] can be used by +/// passing in a list of ticks defined with [TickSpec] of datetime. +class StaticallyProvidedTicks extends StatelessWidget { + final List> seriesList; + final bool animate; + + StaticallyProvidedTicks(this.seriesList, {this.animate = false}); + + factory StaticallyProvidedTicks.withSampleData() { + return new StaticallyProvidedTicks( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StaticallyProvidedTicks.withRandomData() { + return new StaticallyProvidedTicks(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2014', random.nextInt(100) * 100), + new OrdinalSales('2015', random.nextInt(100) * 100), + new OrdinalSales('2016', random.nextInt(100) * 100), + new OrdinalSales('2017', random.nextInt(100) * 100), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Create the ticks to be used the domain axis. + final staticTicks = >[ + new charts.TickSpec( + // Value must match the domain value. + '2014', + // Optional label for this tick, defaults to domain value if not set. + label: 'Year 2014', + // The styling for this tick. + style: new charts.TextStyleSpec( + color: new charts.Color(r: 0x4C, g: 0xAF, b: 0x50))), + // If no text style is specified - the style from renderSpec will be used + // if one is specified. + new charts.TickSpec('2015'), + new charts.TickSpec('2016'), + new charts.TickSpec('2017'), + ]; + + return new charts.BarChart( + seriesList, + animate: animate, + domainAxis: new charts.OrdinalAxisSpec( + tickProviderSpec: + new charts.StaticOrdinalTickProviderSpec(staticTicks)), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 25000), + new OrdinalSales('2016', 100000), + new OrdinalSales('2017', 750000), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/bar_gallery.dart b/example/lib/bar_chart/bar_gallery.dart new file mode 100644 index 0000000..ca656c4 --- /dev/null +++ b/example/lib/bar_chart/bar_gallery.dart @@ -0,0 +1,164 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'custom_rounded_bars.dart'; +import 'grouped.dart'; +import 'grouped_fill_color.dart'; +import 'grouped_single_target_line.dart'; +import 'grouped_stacked.dart'; +import 'grouped_stacked_weight_pattern.dart'; +import 'grouped_target_line.dart'; +import 'horizontal.dart'; +import 'horizontal_bar_label.dart'; +import 'horizontal_bar_label_custom.dart'; +import 'horizontal_pattern_forward_hatch.dart'; +import 'pattern_forward_hatch.dart'; +import 'simple.dart'; +import 'stacked.dart'; +import 'stacked_fill_color.dart'; +import 'stacked_horizontal.dart'; +import 'stacked_target_line.dart'; +import 'spark_bar.dart'; +import 'vertical_bar_label.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Simple Bar Chart', + subtitle: 'Simple bar chart with a single series', + childBuilder: () => new SimpleBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Stacked Bar Chart', + subtitle: 'Stacked bar chart with multiple series', + childBuilder: () => new StackedBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Bar Chart', + subtitle: 'Grouped bar chart with multiple series', + childBuilder: () => new GroupedBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Stacked Bar Chart', + subtitle: 'Grouped and stacked bar chart with multiple series', + childBuilder: () => new GroupedStackedBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Bar Target Line Chart', + subtitle: 'Grouped bar target line chart with multiple series', + childBuilder: () => new GroupedBarTargetLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Bar Single Target Line Chart', + subtitle: + 'Grouped bar target line chart with multiple series and a single target', + childBuilder: () => new GroupedBarSingleTargetLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Stacked Bar Target Line Chart', + subtitle: 'Stacked bar target line chart with multiple series', + childBuilder: () => new StackedBarTargetLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal Bar Chart', + subtitle: 'Horizontal bar chart with a single series', + childBuilder: () => new HorizontalBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Stacked Horizontal Bar Chart', + subtitle: 'Stacked horizontal bar chart with multiple series', + childBuilder: () => new StackedHorizontalBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal Bar Chart with Bar Labels', + subtitle: 'Horizontal bar chart with a single series and bar labels', + childBuilder: () => new HorizontalBarLabelChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal Bar Chart with Custom Bar Labels', + subtitle: 'Bar labels with customized styling', + childBuilder: () => new HorizontalBarLabelCustomChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Vertical Bar Chart with Bar Labels', + subtitle: 'Vertical bar chart with a single series and bar labels', + childBuilder: () => new VerticalBarLabelChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Spark Bar Chart', + subtitle: 'Spark Bar Chart', + childBuilder: () => new SparkBar.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Grouped Fill Color Bar Chart', + subtitle: 'Grouped bar chart with fill colors', + childBuilder: () => new GroupedFillColorBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Stacked Fill Color Bar Chart', + subtitle: 'Stacked bar chart with fill colors', + childBuilder: () => new StackedFillColorBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Pattern Forward Hatch Bar Chart', + subtitle: 'Pattern Forward Hatch Bar Chart', + childBuilder: () => new PatternForwardHatchBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Transform.rotate( + angle: 1.5708, child: new Icon(Icons.insert_chart)), + title: 'Horizontal Pattern Forward Hatch Bar Chart', + subtitle: 'Horizontal Pattern Forward Hatch Bar Chart', + childBuilder: () => + new HorizontalPatternForwardHatchBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Weight Pattern Bar Chart', + subtitle: 'Grouped and stacked bar chart with a weight pattern', + childBuilder: () => + new GroupedStackedWeightPatternBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Bar Chart with custom bar radius', + subtitle: 'Custom rounded bar corners', + childBuilder: () => new CustomRoundedBars.withRandomData(), + ), + ]; +} diff --git a/example/lib/bar_chart/custom_rounded_bars.dart b/example/lib/bar_chart/custom_rounded_bars.dart new file mode 100644 index 0000000..c773501 --- /dev/null +++ b/example/lib/bar_chart/custom_rounded_bars.dart @@ -0,0 +1,110 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class CustomRoundedBars extends StatelessWidget { + final List> seriesList; + final bool animate; + + CustomRoundedBars(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with custom rounded bars. + factory CustomRoundedBars.withSampleData() { + return new CustomRoundedBars( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory CustomRoundedBars.withRandomData() { + return new CustomRoundedBars(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + defaultRenderer: new charts.BarRendererConfig( + // By default, bar renderer will draw rounded bars with a constant + // radius of 30. + // To not have any rounded corners, use [NoCornerStrategy] + // To change the radius of the bars, use [ConstCornerStrategy] + cornerStrategy: const charts.ConstCornerStrategy(30)), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/grouped.dart b/example/lib/bar_chart/grouped.dart new file mode 100644 index 0000000..f61282e --- /dev/null +++ b/example/lib/bar_chart/grouped.dart @@ -0,0 +1,154 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + GroupedBarChart(this.seriesList, {this.animate = false}); + + factory GroupedBarChart.withSampleData() { + return new GroupedBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedBarChart.withRandomData() { + return new GroupedBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/grouped_fill_color.dart b/example/lib/bar_chart/grouped_fill_color.dart new file mode 100644 index 0000000..14a921b --- /dev/null +++ b/example/lib/bar_chart/grouped_fill_color.dart @@ -0,0 +1,178 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of a grouped bar chart with three series, each rendered with +/// different fill colors. +class GroupedFillColorBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + GroupedFillColorBarChart(this.seriesList, {this.animate = false}); + + factory GroupedFillColorBarChart.withSampleData() { + return new GroupedFillColorBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedFillColorBarChart.withRandomData() { + return new GroupedFillColorBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + // Blue bars with a lighter center color. + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + fillColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + ), + // Solid red bars. Fill color will default to the series color if no + // fillColorFn is configured. + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + ), + // Hollow green bars. + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + fillColorFn: (_, __) => charts.MaterialPalette.transparent, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Configure a stroke width to enable borders on the bars. + defaultRenderer: new charts.BarRendererConfig( + groupingType: charts.BarGroupingType.grouped, strokeWidthPx: 2.0), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + // Blue bars with a lighter center color. + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + fillColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + ), + // Solid red bars. Fill color will default to the series color if no + // fillColorFn is configured. + new charts.Series( + id: 'Tablet', + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + ), + // Hollow green bars. + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + fillColorFn: (_, __) => charts.MaterialPalette.transparent, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/grouped_single_target_line.dart b/example/lib/bar_chart/grouped_single_target_line.dart new file mode 100644 index 0000000..1d6012a --- /dev/null +++ b/example/lib/bar_chart/grouped_single_target_line.dart @@ -0,0 +1,180 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedBarSingleTargetLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + GroupedBarSingleTargetLineChart(this.seriesList, {this.animate = false}); + + factory GroupedBarSingleTargetLineChart.withSampleData() { + return new GroupedBarSingleTargetLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedBarSingleTargetLineChart.withRandomData() { + return new GroupedBarSingleTargetLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final targetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: targetLineData) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart(seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + customSeriesRenderers: [ + new charts.BarTargetLineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customTargetLine', + groupingType: charts.BarGroupingType.grouped) + ]); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final targetLineData = [ + new OrdinalSales('2014', 30), + new OrdinalSales('2015', 55), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 25), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: targetLineData) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/grouped_stacked.dart b/example/lib/bar_chart/grouped_stacked.dart new file mode 100644 index 0000000..32551c7 --- /dev/null +++ b/example/lib/bar_chart/grouped_stacked.dart @@ -0,0 +1,244 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a bar chart with grouped, stacked series oriented vertically. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedStackedBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + GroupedStackedBarChart(this.seriesList, {this.animate = false}); + + factory GroupedStackedBarChart.withSampleData() { + return new GroupedStackedBarChart( + createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedStackedBarChart.withRandomData() { + return new GroupedStackedBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.groupedStacked, + ); + } + + /// Create series list with multiple series + static List> createSampleData() { + final desktopSalesDataA = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/grouped_stacked_weight_pattern.dart b/example/lib/bar_chart/grouped_stacked_weight_pattern.dart new file mode 100644 index 0000000..ddccb56 --- /dev/null +++ b/example/lib/bar_chart/grouped_stacked_weight_pattern.dart @@ -0,0 +1,256 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a bar chart with grouped, stacked series oriented vertically with +/// a custom 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. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedStackedWeightPatternBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + GroupedStackedWeightPatternBarChart(this.seriesList, {this.animate = false}); + + factory GroupedStackedWeightPatternBarChart.withSampleData() { + return new GroupedStackedWeightPatternBarChart( + createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedStackedWeightPatternBarChart.withRandomData() { + return new GroupedStackedWeightPatternBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Configure the bar renderer in grouped stacked rendering mode with a + // custom weight pattern. + // + // The first stack of bars in each group is configured to be twice as wide + // as the second stack of bars in each group. + defaultRenderer: new charts.BarRendererConfig( + groupingType: charts.BarGroupingType.groupedStacked, + weightPattern: [2, 1], + ), + ); + } + + /// Create series list with multiple series + static List> createSampleData() { + final desktopSalesDataA = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/grouped_target_line.dart b/example/lib/bar_chart/grouped_target_line.dart new file mode 100644 index 0000000..d4fb620 --- /dev/null +++ b/example/lib/bar_chart/grouped_target_line.dart @@ -0,0 +1,248 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class GroupedBarTargetLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + GroupedBarTargetLineChart(this.seriesList, {this.animate = false}); + + factory GroupedBarTargetLineChart.withSampleData() { + return new GroupedBarTargetLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GroupedBarTargetLineChart.withRandomData() { + return new GroupedBarTargetLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Tablet Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Mobile Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart(seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + customSeriesRenderers: [ + new charts.BarTargetLineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customTargetLine', + groupingType: charts.BarGroupingType.grouped) + ]); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopTargetLineData = [ + new OrdinalSales('2014', 4), + new OrdinalSales('2015', 20), + new OrdinalSales('2016', 80), + new OrdinalSales('2017', 65), + ]; + + final tableTargetLineData = [ + new OrdinalSales('2014', 30), + new OrdinalSales('2015', 55), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 25), + ]; + + final mobileTargetLineData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 5), + new OrdinalSales('2016', 45), + new OrdinalSales('2017', 35), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Tablet Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Mobile Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/horizontal.dart b/example/lib/bar_chart/horizontal.dart new file mode 100644 index 0000000..0d6260a --- /dev/null +++ b/example/lib/bar_chart/horizontal.dart @@ -0,0 +1,104 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Horizontal bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class HorizontalBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + HorizontalBarChart(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory HorizontalBarChart.withSampleData() { + return new HorizontalBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalBarChart.withRandomData() { + return new HorizontalBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // For horizontal bar charts, set the [vertical] flag to false. + return new charts.BarChart( + seriesList, + animate: animate, + vertical: false, + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/horizontal_bar_label.dart b/example/lib/bar_chart/horizontal_bar_label.dart new file mode 100644 index 0000000..e2e9d16 --- /dev/null +++ b/example/lib/bar_chart/horizontal_bar_label.dart @@ -0,0 +1,123 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Horizontal bar chart with bar label renderer example and hidden domain axis. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class HorizontalBarLabelChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + HorizontalBarLabelChart(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory HorizontalBarLabelChart.withSampleData() { + return new HorizontalBarLabelChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalBarLabelChart.withRandomData() { + return new HorizontalBarLabelChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '${sales.year}: \$${sales.sales.toString()}') + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // [BarLabelDecorator] will automatically position the label + // inside the bar if the label will fit. If the label will not fit and the + // area outside of the bar is larger than the bar, it will draw outside of the + // bar. Labels can always display inside or outside using [LabelPosition]. + // + // Text style for inside / outside can be controlled independently by setting + // [insideLabelStyleSpec] and [outsideLabelStyleSpec]. + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + vertical: false, + // Set a bar label decorator. + // Example configuring different styles for inside/outside: + // barRendererDecorator: new charts.BarLabelDecorator( + // insideLabelStyleSpec: new charts.TextStyleSpec(...), + // outsideLabelStyleSpec: new charts.TextStyleSpec(...)), + barRendererDecorator: new charts.BarLabelDecorator(), + // Hide domain axis. + domainAxis: + new charts.OrdinalAxisSpec(renderSpec: new charts.NoneRenderSpec()), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '${sales.year}: \$${sales.sales.toString()}') + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/horizontal_bar_label_custom.dart b/example/lib/bar_chart/horizontal_bar_label_custom.dart new file mode 100644 index 0000000..54583d8 --- /dev/null +++ b/example/lib/bar_chart/horizontal_bar_label_custom.dart @@ -0,0 +1,140 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Horizontal bar chart with custom style for each datum in the bar label. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class HorizontalBarLabelCustomChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + HorizontalBarLabelCustomChart(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + static HorizontalBarLabelCustomChart createWithSampleData() { + return new HorizontalBarLabelCustomChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalBarLabelCustomChart.withRandomData() { + return new HorizontalBarLabelCustomChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '${sales.year}: \$${sales.sales.toString()}', + insideLabelStyleAccessorFn: (OrdinalSales sales, _) { + final color = (sales.year == '2014') + ? charts.MaterialPalette.red.shadeDefault + : charts.MaterialPalette.yellow.shadeDefault.darker; + return new charts.TextStyleSpec(color: color); + }, + outsideLabelStyleAccessorFn: (OrdinalSales sales, _) { + final color = (sales.year == '2014') + ? charts.MaterialPalette.red.shadeDefault + : charts.MaterialPalette.yellow.shadeDefault.darker; + return new charts.TextStyleSpec(color: color); + }, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // The [BarLabelDecorator] has settings to set the text style for all labels + // for inside the bar and outside the bar. To be able to control each datum's + // style, set the style accessor functions on the series. + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + vertical: false, + barRendererDecorator: new charts.BarLabelDecorator(), + // Hide domain axis. + domainAxis: + new charts.OrdinalAxisSpec(renderSpec: new charts.NoneRenderSpec()), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '${sales.year}: \$${sales.sales.toString()}', + insideLabelStyleAccessorFn: (OrdinalSales sales, _) { + final color = (sales.year == '2014') + ? charts.MaterialPalette.red.shadeDefault + : charts.MaterialPalette.yellow.shadeDefault.darker; + return new charts.TextStyleSpec(color: color); + }, + outsideLabelStyleAccessorFn: (OrdinalSales sales, _) { + final color = (sales.year == '2014') + ? charts.MaterialPalette.red.shadeDefault + : charts.MaterialPalette.yellow.shadeDefault.darker; + return new charts.TextStyleSpec(color: color); + }, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/horizontal_pattern_forward_hatch.dart b/example/lib/bar_chart/horizontal_pattern_forward_hatch.dart new file mode 100644 index 0000000..68a7dd8 --- /dev/null +++ b/example/lib/bar_chart/horizontal_pattern_forward_hatch.dart @@ -0,0 +1,164 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Forward pattern hatch bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Forward hatch pattern horizontal bar chart example. +/// +/// The second series of bars is rendered with a pattern by defining a +/// fillPatternFn mapping function. +class HorizontalPatternForwardHatchBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + HorizontalPatternForwardHatchBarChart(this.seriesList, + {this.animate = false}); + + factory HorizontalPatternForwardHatchBarChart.withSampleData() { + return new HorizontalPatternForwardHatchBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory HorizontalPatternForwardHatchBarChart.withRandomData() { + return new HorizontalPatternForwardHatchBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + fillPatternFn: (OrdinalSales sales, _) => + charts.FillPatternType.forwardHatch, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + vertical: false, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + fillPatternFn: (OrdinalSales sales, _) => + charts.FillPatternType.forwardHatch, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/pattern_forward_hatch.dart b/example/lib/bar_chart/pattern_forward_hatch.dart new file mode 100644 index 0000000..a86c975 --- /dev/null +++ b/example/lib/bar_chart/pattern_forward_hatch.dart @@ -0,0 +1,161 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Forward hatch pattern bar chart example. +/// +/// The second series of bars is rendered with a pattern by defining a +/// fillPatternFn mapping function. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class PatternForwardHatchBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PatternForwardHatchBarChart(this.seriesList, {this.animate = false}); + + factory PatternForwardHatchBarChart.withSampleData() { + return new PatternForwardHatchBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PatternForwardHatchBarChart.withRandomData() { + return new PatternForwardHatchBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + fillPatternFn: (OrdinalSales sales, _) => + charts.FillPatternType.forwardHatch, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + fillPatternFn: (OrdinalSales sales, _) => + charts.FillPatternType.forwardHatch, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/simple.dart b/example/lib/bar_chart/simple.dart new file mode 100644 index 0000000..2a99b2b --- /dev/null +++ b/example/lib/bar_chart/simple.dart @@ -0,0 +1,104 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SimpleBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleBarChart(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory SimpleBarChart.withSampleData() { + return new SimpleBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleBarChart.withRandomData() { + return new SimpleBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/spark_bar.dart b/example/lib/bar_chart/spark_bar.dart new file mode 100644 index 0000000..a3e9fc6 --- /dev/null +++ b/example/lib/bar_chart/spark_bar.dart @@ -0,0 +1,140 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Spark Bar Example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of a Spark Bar by hiding both axis, reducing the chart margins. +class SparkBar extends StatelessWidget { + final List> seriesList; + final bool animate; + + SparkBar(this.seriesList, {this.animate = false}); + + factory SparkBar.withSampleData() { + return new SparkBar( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SparkBar.withRandomData() { + return new SparkBar(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final globalSalesData = [ + new OrdinalSales('2007', random.nextInt(100)), + new OrdinalSales('2008', random.nextInt(100)), + new OrdinalSales('2009', random.nextInt(100)), + new OrdinalSales('2010', random.nextInt(100)), + new OrdinalSales('2011', random.nextInt(100)), + new OrdinalSales('2012', random.nextInt(100)), + new OrdinalSales('2013', random.nextInt(100)), + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + + /// Assign a custom style for the measure axis. + /// + /// The NoneRenderSpec only draws an axis line (and even that can be hidden + /// with showAxisLine=false). + primaryMeasureAxis: + new charts.NumericAxisSpec(renderSpec: new charts.NoneRenderSpec()), + + /// This is an OrdinalAxisSpec to match up with BarChart's default + /// ordinal domain axis (use NumericAxisSpec or DateTimeAxisSpec for + /// other charts). + domainAxis: new charts.OrdinalAxisSpec( + // Make sure that we draw the domain axis line. + showAxisLine: true, + // But don't draw anything else. + renderSpec: new charts.NoneRenderSpec()), + + // With a spark chart we likely don't want large chart margins. + // 1px is the smallest we can make each margin. + layoutConfig: new charts.LayoutConfig( + leftMarginSpec: new charts.MarginSpec.fixedPixel(0), + topMarginSpec: new charts.MarginSpec.fixedPixel(0), + rightMarginSpec: new charts.MarginSpec.fixedPixel(0), + bottomMarginSpec: new charts.MarginSpec.fixedPixel(0)), + ); + } + + /// Create series list with single series + static List> _createSampleData() { + final globalSalesData = [ + new OrdinalSales('2007', 3100), + new OrdinalSales('2008', 3500), + new OrdinalSales('2009', 5000), + new OrdinalSales('2010', 2500), + new OrdinalSales('2011', 3200), + new OrdinalSales('2012', 4500), + new OrdinalSales('2013', 4400), + new OrdinalSales('2014', 5000), + new OrdinalSales('2015', 5000), + new OrdinalSales('2016', 4500), + new OrdinalSales('2017', 4300), + ]; + + return [ + new charts.Series( + id: 'Global Revenue', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: globalSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/stacked.dart b/example/lib/bar_chart/stacked.dart new file mode 100644 index 0000000..c598e9f --- /dev/null +++ b/example/lib/bar_chart/stacked.dart @@ -0,0 +1,155 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class StackedBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedBarChart(this.seriesList, {this.animate = false}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory StackedBarChart.withSampleData() { + return new StackedBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedBarChart.withRandomData() { + return new StackedBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.stacked, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/stacked_fill_color.dart b/example/lib/bar_chart/stacked_fill_color.dart new file mode 100644 index 0000000..cd4e596 --- /dev/null +++ b/example/lib/bar_chart/stacked_fill_color.dart @@ -0,0 +1,178 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example of a stacked bar chart with three series, each rendered with +/// different fill colors. +class StackedFillColorBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedFillColorBarChart(this.seriesList, {this.animate = false}); + + factory StackedFillColorBarChart.withSampleData() { + return new StackedFillColorBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedFillColorBarChart.withRandomData() { + return new StackedFillColorBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + // Blue bars with a lighter center color. + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + fillColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + ), + // Solid red bars. Fill color will default to the series color if no + // fillColorFn is configured. + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + ), + // Hollow green bars. + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + fillColorFn: (_, __) => charts.MaterialPalette.transparent, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Configure a stroke width to enable borders on the bars. + defaultRenderer: new charts.BarRendererConfig( + groupingType: charts.BarGroupingType.stacked, strokeWidthPx: 2.0), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + // Blue bars with a lighter center color. + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + fillColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + ), + // Solid red bars. Fill color will default to the series color if no + // fillColorFn is configured. + new charts.Series( + id: 'Tablet', + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + ), + // Hollow green bars. + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + fillColorFn: (_, __) => charts.MaterialPalette.transparent, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/stacked_horizontal.dart b/example/lib/bar_chart/stacked_horizontal.dart new file mode 100644 index 0000000..0f20b9a --- /dev/null +++ b/example/lib/bar_chart/stacked_horizontal.dart @@ -0,0 +1,157 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class StackedHorizontalBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedHorizontalBarChart(this.seriesList, {this.animate = false}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory StackedHorizontalBarChart.withSampleData() { + return new StackedHorizontalBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedHorizontalBarChart.withRandomData() { + return new StackedHorizontalBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // For horizontal bar charts, set the [vertical] flag to false. + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.stacked, + vertical: false, + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/stacked_target_line.dart b/example/lib/bar_chart/stacked_target_line.dart new file mode 100644 index 0000000..01dc9e8 --- /dev/null +++ b/example/lib/bar_chart/stacked_target_line.dart @@ -0,0 +1,249 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class StackedBarTargetLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedBarTargetLineChart(this.seriesList, {this.animate = false}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory StackedBarTargetLineChart.withSampleData() { + return new StackedBarTargetLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedBarTargetLineChart.withRandomData() { + return new StackedBarTargetLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileTargetLineData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Tablet Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Mobile Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart(seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.stacked, + customSeriesRenderers: [ + new charts.BarTargetLineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customTargetLine', + groupingType: charts.BarGroupingType.stacked) + ]); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopTargetLineData = [ + new OrdinalSales('2014', 4), + new OrdinalSales('2015', 20), + new OrdinalSales('2016', 80), + new OrdinalSales('2017', 65), + ]; + + final tableTargetLineData = [ + new OrdinalSales('2014', 30), + new OrdinalSales('2015', 55), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 25), + ]; + + final mobileTargetLineData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 5), + new OrdinalSales('2016', 45), + new OrdinalSales('2017', 35), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Desktop Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Tablet Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + new charts.Series( + id: 'Mobile Target Line', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileTargetLineData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customTargetLine'), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/bar_chart/vertical_bar_label.dart b/example/lib/bar_chart/vertical_bar_label.dart new file mode 100644 index 0000000..3c53ca8 --- /dev/null +++ b/example/lib/bar_chart/vertical_bar_label.dart @@ -0,0 +1,120 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Vertical bar chart with bar label renderer example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class VerticalBarLabelChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + VerticalBarLabelChart(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory VerticalBarLabelChart.withSampleData() { + return new VerticalBarLabelChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory VerticalBarLabelChart.withRandomData() { + return new VerticalBarLabelChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '${sales.sales.toString()}') + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // [BarLabelDecorator] will automatically position the label + // inside the bar if the label will fit. If the label will not fit, + // it will draw outside of the bar. + // Labels can always display inside or outside using [LabelPosition]. + // + // Text style for inside / outside can be controlled independently by setting + // [insideLabelStyleSpec] and [outsideLabelStyleSpec]. + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Set a bar label decorator. + // Example configuring different styles for inside/outside: + // barRendererDecorator: new charts.BarLabelDecorator( + // insideLabelStyleSpec: new charts.TextStyleSpec(...), + // outsideLabelStyleSpec: new charts.TextStyleSpec(...)), + barRendererDecorator: new charts.BarLabelDecorator(), + domainAxis: new charts.OrdinalAxisSpec(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the bar label. + labelAccessorFn: (OrdinalSales sales, _) => + '\$${sales.sales.toString()}') + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/behaviors_gallery.dart b/example/lib/behaviors/behaviors_gallery.dart new file mode 100644 index 0000000..36d25e9 --- /dev/null +++ b/example/lib/behaviors/behaviors_gallery.dart @@ -0,0 +1,126 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'chart_title.dart'; +import 'initial_hint_animation.dart'; +import 'initial_selection.dart'; +import 'percent_of_domain.dart'; +import 'percent_of_domain_by_category.dart'; +import 'percent_of_series.dart'; +import 'selection_bar_highlight.dart'; +import 'selection_line_highlight.dart'; +import 'selection_line_highlight_custom_shape.dart'; +import 'selection_callback_example.dart'; +import 'selection_scatter_plot_highlight.dart'; +import 'selection_user_managed.dart'; +import 'slider.dart'; +import 'sliding_viewport_on_selection.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Bar Highlight', + subtitle: 'Simple bar chart with tap activation', + childBuilder: () => new SelectionBarHighlight.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Line Highlight', + subtitle: 'Line chart with tap and drag activation', + childBuilder: () => new SelectionLineHighlight.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Line Highlight Custom Shape', + subtitle: 'Line chart with tap and drag activation and a custom shape', + childBuilder: () => + new SelectionLineHighlightCustomShape.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Scatter Plot Highlight', + subtitle: 'Scatter plot chart with tap and drag activation', + childBuilder: () => new SelectionScatterPlotHighlight.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Selection Callback Example', + subtitle: 'Timeseries that updates external components on selection', + childBuilder: () => new SelectionCallbackExample.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'User managed selection', + subtitle: + 'Example where selection can be set and cleared programmatically', + childBuilder: () => new SelectionUserManaged.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Bar Chart with initial selection', + subtitle: 'Single series with initial selection', + childBuilder: () => new InitialSelection.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Line Chart with Chart Titles', + subtitle: 'Line chart with four chart titles', + childBuilder: () => new ChartTitleLine.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'Line Chart with Slider', + subtitle: 'Line chart with a slider behavior', + childBuilder: () => new SliderLine.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Percent of Domain', + subtitle: 'Stacked bar chart with measures calculated as percent of ' + + 'domain', + childBuilder: () => new PercentOfDomainBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Percent of Domain by Category', + subtitle: 'Grouped stacked bar chart with measures calculated as ' + 'percent of domain and series category', + childBuilder: () => + new PercentOfDomainByCategoryBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Percent of Series', + subtitle: 'Grouped bar chart with measures calculated as percent of ' + + 'series', + childBuilder: () => new PercentOfSeriesBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Sliding viewport on domain selection', + subtitle: 'Center viewport on selected domain', + childBuilder: () => new SlidingViewportOnSelection.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Initial hint animation ', + subtitle: 'Animate into final viewport', + childBuilder: () => new InitialHintAnimation.withRandomData(), + ), + ]; +} diff --git a/example/lib/behaviors/chart_title.dart b/example/lib/behaviors/chart_title.dart new file mode 100644 index 0000000..ad652f5 --- /dev/null +++ b/example/lib/behaviors/chart_title.dart @@ -0,0 +1,131 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +/// This is a line chart with a title text in every margin. +/// +/// A series of [ChartTitle] behaviors are used to render titles, one per +/// margin. +class ChartTitleLine extends StatelessWidget { + final List> seriesList; + final bool animate; + + ChartTitleLine(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory ChartTitleLine.withSampleData() { + return new ChartTitleLine( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ChartTitleLine.withRandomData() { + return new ChartTitleLine(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart( + seriesList, + animate: animate, + // Configures four [ChartTitle] behaviors to render titles in each chart + // margin. The top title has a sub-title, and is aligned to the left edge + // of the chart. The other titles are aligned with the middle of the draw + // area. + behaviors: [ + new charts.ChartTitle('Top title text', + subTitle: 'Top sub-title text', + behaviorPosition: charts.BehaviorPosition.top, + titleOutsideJustification: charts.OutsideJustification.start, + // Set a larger inner padding than the default (10) to avoid + // rendering the text too close to the top measure axis tick label. + // The top tick label may extend upwards into the top margin region + // if it is located at the top of the draw area. + innerPadding: 18), + new charts.ChartTitle('Bottom title text', + behaviorPosition: charts.BehaviorPosition.bottom, + titleOutsideJustification: + charts.OutsideJustification.middleDrawArea), + new charts.ChartTitle('Start title', + behaviorPosition: charts.BehaviorPosition.start, + titleOutsideJustification: + charts.OutsideJustification.middleDrawArea), + new charts.ChartTitle('End title', + behaviorPosition: charts.BehaviorPosition.end, + titleOutsideJustification: + charts.OutsideJustification.middleDrawArea), + ], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/initial_hint_animation.dart b/example/lib/behaviors/initial_hint_animation.dart new file mode 100644 index 0000000..804a006 --- /dev/null +++ b/example/lib/behaviors/initial_hint_animation.dart @@ -0,0 +1,175 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of initial hint animation behavior. +/// +/// To see the animation, please run the example app and select +/// "Initial hint animation". +/// +/// This behavior is intended to be used with charts that also have pan/zoom +/// behaviors added and/or the initial viewport set in [AxisSpec]. +/// +/// Adding this behavior will cause the chart to animate from a scale and/or +/// offset of the desired final viewport. If the user taps the widget prior +/// to the animation being completed, animation will stop. +/// +/// [maxHintScaleFactor] is the amount the domain axis will be scaled at the +/// start of te hint. By default, this is null, indicating that there will be +/// no scale factor hint. A value of 1.0 means the viewport is showing all +/// domains in the viewport. If a value is provided, it cannot be less than 1.0. +/// +/// [maxHintTranslate] is the amount of ordinal values to translate the viewport +/// from the desired initial viewport. Currently only works for ordinal axis. +/// +/// In this example, the series list has ordinal data from year 2014 to 2030, +/// and we have the initial viewport set to start at 2018 that shows 4 values by +/// specifying an [OrdinalViewport] in [OrdinalAxisSpec]. We can add the hint +/// animation by adding behavior [InitialHintBehavior] with [maxHintTranslate] +/// of 4. When the chart is drawn for the first time, the viewport will show +/// 2022 as the first value and the viewport will animate by panning values to +/// the right until 2018 is the first value in the viewport. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class InitialHintAnimation extends StatelessWidget { + final List> seriesList; + final bool animate; + + InitialHintAnimation(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory InitialHintAnimation.withSampleData() { + return new InitialHintAnimation( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory InitialHintAnimation.withRandomData() { + return new InitialHintAnimation(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + new OrdinalSales('2018', random.nextInt(100)), + new OrdinalSales('2019', random.nextInt(100)), + new OrdinalSales('2020', random.nextInt(100)), + new OrdinalSales('2021', random.nextInt(100)), + new OrdinalSales('2022', random.nextInt(100)), + new OrdinalSales('2023', random.nextInt(100)), + new OrdinalSales('2024', random.nextInt(100)), + new OrdinalSales('2025', random.nextInt(100)), + new OrdinalSales('2026', random.nextInt(100)), + new OrdinalSales('2027', random.nextInt(100)), + new OrdinalSales('2028', random.nextInt(100)), + new OrdinalSales('2029', random.nextInt(100)), + new OrdinalSales('2030', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + // Optionally turn off the animation that animates values up from the + // bottom of the domain axis. If animation is on, the bars will animate up + // and then animate to the final viewport. + animationDuration: Duration.zero, + // Set the initial viewport by providing a new AxisSpec with the + // desired viewport: a starting domain and the data size. + domainAxis: new charts.OrdinalAxisSpec( + viewport: new charts.OrdinalViewport('2018', 4)), + behaviors: [ + // Add this behavior to show initial hint animation that will pan to the + // final desired viewport. + // The duration of the animation can be adjusted by pass in + // [hintDuration]. By default this is 3000ms. + new charts.InitialHintBehavior(maxHintTranslate: 4.0), + // Optionally add a pan or pan and zoom behavior. + // If pan/zoom is not added, the viewport specified remains the viewport + new charts.PanAndZoomBehavior(), + ], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + new OrdinalSales('2018', 33), + new OrdinalSales('2019', 80), + new OrdinalSales('2020', 21), + new OrdinalSales('2021', 77), + new OrdinalSales('2022', 8), + new OrdinalSales('2023', 12), + new OrdinalSales('2024', 42), + new OrdinalSales('2025', 70), + new OrdinalSales('2026', 77), + new OrdinalSales('2027', 55), + new OrdinalSales('2028', 19), + new OrdinalSales('2029', 66), + new OrdinalSales('2030', 27), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/initial_selection.dart b/example/lib/behaviors/initial_selection.dart new file mode 100644 index 0000000..8793bee --- /dev/null +++ b/example/lib/behaviors/initial_selection.dart @@ -0,0 +1,129 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of adding an initial selection behavior. +/// +/// This example adds initial selection to a bar chart, but any chart can use +/// the initial selection behavior. +/// +/// Initial selection is only set on the very first draw and will not be set +/// again in subsequent draws unless the behavior is reconfigured. +/// +/// The selection will remain on the chart unless another behavior is added +/// that updates the selection. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class InitialSelection extends StatelessWidget { + final List> seriesList; + final bool animate; + + InitialSelection(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with initial selection behavior. + factory InitialSelection.withSampleData() { + return new InitialSelection( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory InitialSelection.withRandomData() { + return new InitialSelection(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + behaviors: [ + // Initial selection can be configured by passing in: + // + // A list of datum config, specified with series ID and domain value. + // A list of series config, which is a list of series ID(s). + // + // Initial selection can be applied to any chart type. + // + // [BarChart] by default includes behaviors [SelectNearest] and + // [DomainHighlighter]. So this behavior shows the initial selection + // highlighted and when another datum is tapped, the selection changes. + new charts.InitialSelection(selectedDataConfig: [ + new charts.SeriesDatumConfig('Sales', '2016') + ]) + ], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/percent_of_domain.dart b/example/lib/behaviors/percent_of_domain.dart new file mode 100644 index 0000000..2cf836c --- /dev/null +++ b/example/lib/behaviors/percent_of_domain.dart @@ -0,0 +1,167 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a percentage bar chart with stacked series oriented vertically. +/// +/// Each bar stack shows the percentage of each measure out of the total measure +/// value of the stack. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class PercentOfDomainBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PercentOfDomainBarChart(this.seriesList, {this.animate = false}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory PercentOfDomainBarChart.withSampleData() { + return new PercentOfDomainBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PercentOfDomainBarChart.withRandomData() { + return new PercentOfDomainBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.stacked, + // Configures a [PercentInjector] behavior that will calculate measure + // values as the percentage of the total of all data that shares a + // domain value. + behaviors: [ + new charts.PercentInjector( + totalType: charts.PercentInjectorTotalType.domain) + ], + // Configure the axis spec to show percentage values. + primaryMeasureAxis: new charts.PercentAxisSpec(), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/percent_of_domain_by_category.dart b/example/lib/behaviors/percent_of_domain_by_category.dart new file mode 100644 index 0000000..5380c46 --- /dev/null +++ b/example/lib/behaviors/percent_of_domain_by_category.dart @@ -0,0 +1,261 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a percentage bar chart with grouped, stacked series oriented +/// vertically. +/// +/// Each bar stack shows the percentage of each measure out of the total measure +/// value of the stack. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class PercentOfDomainByCategoryBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PercentOfDomainByCategoryBarChart(this.seriesList, {this.animate = false}); + + factory PercentOfDomainByCategoryBarChart.withSampleData() { + return new PercentOfDomainByCategoryBarChart( + createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PercentOfDomainByCategoryBarChart.withRandomData() { + return new PercentOfDomainByCategoryBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.groupedStacked, + // Configures a [PercentInjector] behavior that will calculate measure + // values as the percentage of the total of all data that shares both a + // domain and a series category. + // + // We use this option on a grouped stacked bar chart to ensure that the + // total value for each bar stack is 100%. A stacked bar chart that does + // not group by series category would use the "domain" option. + behaviors: [ + new charts.PercentInjector( + totalType: charts.PercentInjectorTotalType.domainBySeriesCategory) + ], + // Configure the axis spec to show percentage values. + primaryMeasureAxis: new charts.PercentAxisSpec(), + ); + } + + /// Create series list with multiple series + static List> createSampleData() { + final desktopSalesDataA = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataA = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataA = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final desktopSalesDataB = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesDataB = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesDataB = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + return [ + new charts.Series( + id: 'Desktop A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataA, + ), + new charts.Series( + id: 'Tablet A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataA, + ), + new charts.Series( + id: 'Mobile A', + seriesCategory: 'A', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataA, + ), + new charts.Series( + id: 'Desktop B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesDataB, + ), + new charts.Series( + id: 'Tablet B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesDataB, + ), + new charts.Series( + id: 'Mobile B', + seriesCategory: 'B', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesDataB, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/percent_of_series.dart b/example/lib/behaviors/percent_of_series.dart new file mode 100644 index 0000000..92c9461 --- /dev/null +++ b/example/lib/behaviors/percent_of_series.dart @@ -0,0 +1,120 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a percentage bar chart which shows each bar as the percentage of +/// the total series measure value. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class PercentOfSeriesBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PercentOfSeriesBarChart(this.seriesList, {this.animate = false}); + + /// Creates a stacked [BarChart] with sample data and no transition. + factory PercentOfSeriesBarChart.withSampleData() { + return new PercentOfSeriesBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PercentOfSeriesBarChart.withRandomData() { + return new PercentOfSeriesBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Configures a [PercentInjector] behavior that will calculate measure + // values as the percentage of the total of all data in its series. + behaviors: [ + new charts.PercentInjector( + totalType: charts.PercentInjectorTotalType.series) + ], + // Configure the axis spec to show percentage values. + primaryMeasureAxis: new charts.PercentAxisSpec(), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2011', 5), + new OrdinalSales('2012', 25), + new OrdinalSales('2013', 50), + new OrdinalSales('2014', 75), + new OrdinalSales('2015', 100), + new OrdinalSales('2016', 125), + new OrdinalSales('2017', 200), + new OrdinalSales('2018', 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/selection_bar_highlight.dart b/example/lib/behaviors/selection_bar_highlight.dart new file mode 100644 index 0000000..86594a5 --- /dev/null +++ b/example/lib/behaviors/selection_bar_highlight.dart @@ -0,0 +1,110 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SelectionBarHighlight extends StatelessWidget { + final List> seriesList; + final bool animate; + + SelectionBarHighlight(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory SelectionBarHighlight.withSampleData() { + return new SelectionBarHighlight( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionBarHighlight.withRandomData() { + return new SelectionBarHighlight(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // This is just a simple bar chart with optional property + // [defaultInteractions] set to true to include the default + // interactions/behaviors when building the chart. + // This includes bar highlighting. + // + // Note: defaultInteractions defaults to true. + // + // [defaultInteractions] can be set to false to avoid the default + // interactions. + return new charts.BarChart( + seriesList, + animate: animate, + defaultInteractions: true, + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/selection_callback_example.dart b/example/lib/behaviors/selection_callback_example.dart new file mode 100644 index 0000000..d46112b --- /dev/null +++ b/example/lib/behaviors/selection_callback_example.dart @@ -0,0 +1,201 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Timeseries chart with example of updating external state based on selection. +/// +/// A SelectionModelConfig can be provided for each of the different +/// [SelectionModel] (currently info and action). +/// +/// [SelectionModelType.info] is the default selection chart exploration type +/// initiated by some tap event. This is a different model from +/// [SelectionModelType.action] which is typically used to select some value as +/// an input to some other UI component. This allows dual state of exploring +/// and selecting data via different touch events. +/// +/// See [SelectNearest] behavior on setting the different ways of triggering +/// [SelectionModel] updates from hover & click events. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SelectionCallbackExample extends StatefulWidget { + final List> seriesList; + final bool animate; + + SelectionCallbackExample(this.seriesList, {this.animate = false}); + + /// Creates a [charts.TimeSeriesChart] with sample data and no transition. + factory SelectionCallbackExample.withSampleData() { + return new SelectionCallbackExample( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionCallbackExample.withRandomData() { + return new SelectionCallbackExample(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final us_data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + final uk_data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'US Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: us_data, + ), + new charts.Series( + id: 'UK Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: uk_data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // We need a Stateful widget to build the selection details with the current + // selection as the state. + @override + State createState() => new _SelectionCallbackState(); + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final us_data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 78), + new TimeSeriesSales(new DateTime(2017, 10, 10), 54), + ]; + + final uk_data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 15), + new TimeSeriesSales(new DateTime(2017, 9, 26), 33), + new TimeSeriesSales(new DateTime(2017, 10, 3), 68), + new TimeSeriesSales(new DateTime(2017, 10, 10), 48), + ]; + + return [ + new charts.Series( + id: 'US Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: us_data, + ), + new charts.Series( + id: 'UK Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: uk_data, + ) + ]; + } +} + +class _SelectionCallbackState extends State { + DateTime? _time; + Map _measures = {}; + + // Listens to the underlying selection changes, and updates the information + // relevant to building the primitive legend like information under the + // chart. + _onSelectionChanged(charts.SelectionModel model) { + final selectedDatum = model.selectedDatum; + + DateTime? time; + final measures = {}; + + // We get the model that updated with a list of [SeriesDatum] which is + // simply a pair of series & datum. + // + // Walk the selection updating the measures map, storing off the sales and + // series name for each selection point. + if (selectedDatum.isNotEmpty) { + time = selectedDatum.first.datum.time; + selectedDatum.forEach((charts.SeriesDatum datumPair) { + measures[datumPair.series.displayName!] = datumPair.datum.sales; + }); + } + + // Request a build. + setState(() { + _time = time; + _measures = measures; + }); + } + + @override + Widget build(BuildContext context) { + // The children consist of a Chart and Text widgets below to hold the info. + final children = [ + new SizedBox( + height: 150.0, + child: new charts.TimeSeriesChart( + widget.seriesList, + animate: widget.animate, + selectionModels: [ + new charts.SelectionModelConfig( + type: charts.SelectionModelType.info, + changedListener: _onSelectionChanged, + ) + ], + )), + ]; + + // If there is a selection, then include the details. + if (_time != null) { + children.add(new Padding( + padding: new EdgeInsets.only(top: 5.0), + child: new Text(_time.toString()))); + } + _measures.forEach((String series, num value) { + children.add(new Text('${series}: ${value}')); + }); + + return new Column(children: children); + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/example/lib/behaviors/selection_line_highlight.dart b/example/lib/behaviors/selection_line_highlight.dart new file mode 100644 index 0000000..89ae68b --- /dev/null +++ b/example/lib/behaviors/selection_line_highlight.dart @@ -0,0 +1,127 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SelectionLineHighlight extends StatelessWidget { + final List> seriesList; + final bool animate; + + SelectionLineHighlight(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory SelectionLineHighlight.withSampleData() { + return new SelectionLineHighlight( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionLineHighlight.withRandomData() { + return new SelectionLineHighlight(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // This is just a simple line chart with a behavior that highlights the + // selected points along the lines. A point will be drawn at the selected + // datum's x,y coordinate, and a vertical follow line will be drawn through + // it. + // + // A [Charts.LinePointHighlighter] behavior is added manually to enable the + // highlighting effect. + // + // As an alternative, [defaultInteractions] can be set to true to include + // the default chart interactions, including a LinePointHighlighter. + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + // Optional - Configures a [LinePointHighlighter] behavior with a + // vertical follow line. A vertical follow line is included by + // default, but is shown here as an example configuration. + // + // By default, the line has default dash pattern of [1,3]. This can be + // set by providing a [dashPattern] or it can be turned off by passing in + // an empty list. An empty list is necessary because passing in a null + // value will be treated the same as not passing in a value at all. + new charts.LinePointHighlighter( + showHorizontalFollowLine: + charts.LinePointHighlighterFollowLineType.none, + showVerticalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest), + // Optional - By default, select nearest is configured to trigger + // with tap so that a user can have pan/zoom behavior and line point + // highlighter. Changing the trigger to tap and drag allows the + // highlighter to follow the dragging gesture but it is not + // recommended to be used when pan/zoom behavior is enabled. + new charts.SelectNearest(eventTrigger: charts.SelectionTrigger.tapAndDrag) + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/selection_line_highlight_custom_shape.dart b/example/lib/behaviors/selection_line_highlight_custom_shape.dart new file mode 100644 index 0000000..dcf7ae4 --- /dev/null +++ b/example/lib/behaviors/selection_line_highlight_custom_shape.dart @@ -0,0 +1,130 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SelectionLineHighlightCustomShape extends StatelessWidget { + final List> seriesList; + final bool animate; + + SelectionLineHighlightCustomShape(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory SelectionLineHighlightCustomShape.withSampleData() { + return new SelectionLineHighlightCustomShape( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionLineHighlightCustomShape.withRandomData() { + return new SelectionLineHighlightCustomShape(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // This is a simple line chart with a behavior that highlights hovered + // lines. A hollow rectangular shape will be drawn at the hovered datum's + // x,y coordinate, and a vertical follow line will be drawn through it. + // + // A [Charts.LinePointHighlighter] behavior is added manually to enable the + // highlighting effect. + // + // As an alternative, [defaultInteractions] can be set to true to include + // the default chart interactions, including a LinePointHighlighter. + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + // Optional - Configures a [LinePointHighlighter] behavior with a + // vertical follow line. A vertical follow line is included by + // default, but is shown here as an example configuration. + // + // By default, the line has default dash pattern of [1,3]. This can be + // set by providing a [dashPattern] or it can be turned off by passing in + // an empty list. An empty list is necessary because passing in a null + // value will be treated the same as not passing in a value at all. + // + // The symbol renderer is configured to render a hollow shape, for + // demonstration. + new charts.LinePointHighlighter( + showHorizontalFollowLine: + charts.LinePointHighlighterFollowLineType.none, + showVerticalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest, + symbolRenderer: new charts.RectSymbolRenderer(isSolid: false)), + // Optional - By default, select nearest is configured to trigger + // with tap so that a user can have pan/zoom behavior and line point + // highlighter. Changing the trigger to tap and drag allows the + // highlighter to follow the dragging gesture but it is not + // recommended to be used when pan/zoom behavior is enabled. + new charts.SelectNearest(eventTrigger: charts.SelectionTrigger.tapAndDrag) + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/selection_scatter_plot_highlight.dart b/example/lib/behaviors/selection_scatter_plot_highlight.dart new file mode 100644 index 0000000..abf471e --- /dev/null +++ b/example/lib/behaviors/selection_scatter_plot_highlight.dart @@ -0,0 +1,235 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a scatter plot chart using custom symbols for the points and a +/// behavior that highlights selected points. +/// +/// An optional [charts.LinePointHighlighter] behavior has been added to enable +/// a highlighting effect. This behavior will draw a larger symbol on top of the +/// point nearest to the point where a user taps on the chart. It will also draw +/// follow lines. +/// +/// The series has been configured to draw each point as a square by default. +/// +/// Some data will be drawn as a circle, indicated by defining a custom "circle" +/// value referenced by [charts.pointSymbolRendererFnKey]. +/// +/// Some other data have will be drawn as a hollow circle. In addition to the +/// custom renderer key, these data also have stroke and fillColor values +/// defined. Configuring a separate fillColor will cause the center of the shape +/// to be filled in, with white in these examples. The border of the shape will +/// be color with the color of the data. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SelectionScatterPlotHighlight extends StatelessWidget { + final List> seriesList; + final bool animate; + + SelectionScatterPlotHighlight(this.seriesList, {this.animate = false}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory SelectionScatterPlotHighlight.withSampleData() { + return new SelectionScatterPlotHighlight( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionScatterPlotHighlight.withRandomData() { + return new SelectionScatterPlotHighlight(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + final data = [ + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + 'circle', null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + 'circle', null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow square, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, null, charts.MaterialPalette.white, 2.0), + ]; + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + fillColorFn: (LinearSales row, _) => row.fillColor, + strokeWidthPxFn: (LinearSales row, _) => row.strokeWidth, + data: data, + ) + // Accessor function that associates each datum with a symbol renderer. + ..setAttribute( + charts.pointSymbolRendererFnKey, (int index) => data[index].shape) + // Default symbol renderer ID for data that have no defined shape. + ..setAttribute(charts.pointSymbolRendererIdKey, 'rect') + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + behaviors: [ + // Optional - Configures a [LinePointHighlighter] behavior with + // horizontal and vertical follow lines. The highlighter will increase + // the size of the selected points on the chart. + // + // By default, the line has default dash pattern of [1,3]. This can be + // set by providing a [dashPattern] or it can be turned off by passing + // in an empty list. An empty list is necessary because passing in a + // null value will be treated the same as not passing in a value at + // all. + new charts.LinePointHighlighter( + showHorizontalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest, + showVerticalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest), + // Optional - By default, select nearest is configured to trigger + // with tap so that a user can have pan/zoom behavior and line point + // highlighter. Changing the trigger to tap and drag allows the + // highlighter to follow the dragging gesture but it is not + // recommended to be used when pan/zoom behavior is enabled. + new charts.SelectNearest( + eventTrigger: charts.SelectionTrigger.tapAndDrag), + ], + // Configure the point renderer to have a map of custom symbol + // renderers. + defaultRenderer: + new charts.PointRendererConfig(customSymbolRenderers: { + 'circle': new charts.CircleSymbolRenderer(), + 'rect': new charts.RectSymbolRenderer(), + })); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5, 3.0, 'circle', null, null), + new LinearSales(10, 25, 5.0, null, null, null), + new LinearSales(12, 75, 4.0, null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales( + 13, 225, 5.0, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(16, 50, 4.0, null, null, null), + new LinearSales(24, 75, 3.0, null, null, null), + new LinearSales(25, 100, 3.0, 'circle', null, null), + new LinearSales(34, 150, 5.0, null, null, null), + new LinearSales(37, 10, 4.5, null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales( + 45, 300, 8.0, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(52, 15, 4.0, null, null, null), + // Render a hollow square, filled in with white. + new LinearSales(56, 200, 7.0, null, charts.MaterialPalette.white, 2.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + fillColorFn: (LinearSales row, _) => row.fillColor, + strokeWidthPxFn: (LinearSales row, _) => row.strokeWidth, + data: data, + ) + // Accessor function that associates each datum with a symbol renderer. + ..setAttribute( + charts.pointSymbolRendererFnKey, (int index) => data[index].shape) + // Default symbol renderer ID for data that have no defined shape. + ..setAttribute(charts.pointSymbolRendererIdKey, 'rect') + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + final String? shape; + final charts.Color? fillColor; + final double? strokeWidth; + + LinearSales(this.year, this.sales, this.radius, this.shape, this.fillColor, + this.strokeWidth); +} diff --git a/example/lib/behaviors/selection_user_managed.dart b/example/lib/behaviors/selection_user_managed.dart new file mode 100644 index 0000000..f378915 --- /dev/null +++ b/example/lib/behaviors/selection_user_managed.dart @@ -0,0 +1,164 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of using user managed state to programmatically set selection. +/// +/// In this example, clicking the "clear selection" button sets the selection +/// to an empty selection. This example also shows that initial selection +/// behavior can still be used with user managed state. +/// +/// Note that the picture in this example is not interactive, please run the +/// gallery app to try out using the button to clear selection. +/// +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SelectionUserManaged extends StatefulWidget { + final List> seriesList; + final bool animate; + + SelectionUserManaged(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory SelectionUserManaged.withSampleData() { + return new SelectionUserManaged( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SelectionUserManaged.withRandomData() { + return new SelectionUserManaged(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + + @override + SelectionUserManagedState createState() { + return new SelectionUserManagedState(); + } +} + +class SelectionUserManagedState extends State { + final _myState = new charts.UserManagedState(); + + @override + Widget build(BuildContext context) { + final chart = new charts.BarChart( + widget.seriesList, + animate: false, //widget.animate, + selectionModels: [ + new charts.SelectionModelConfig( + type: charts.SelectionModelType.info, + updatedListener: _infoSelectionModelUpdated) + ], + // Pass in the state you manage to the chart. This will be used to + // override the internal chart state. + userManagedState: _myState, + // The initial selection can still be optionally added by adding the + // initial selection behavior. + behaviors: [ + new charts.InitialSelection(selectedDataConfig: [ + new charts.SeriesDatumConfig('Sales', '2016') + ]) + ], + ); + + final clearSelection = new MaterialButton( + onPressed: _handleClearSelection, child: new Text('Clear Selection')); + + return new Column( + children: [new SizedBox(height: 150.0, child: chart), clearSelection]); + } + + void _infoSelectionModelUpdated(charts.SelectionModel model) { + // If you want to allow the chart to continue to respond to select events + // that update the selection, add an updatedListener that saves off the + // selection model each time the selection model is updated, regardless of + // if there are changes. + // + // This also allows you to listen to the selection model update events and + // alter the selection. + _myState.selectionModels[charts.SelectionModelType.info] = + new charts.UserManagedSelectionModel(model: model); + } + + void _handleClearSelection() { + // Call set state to request a rebuild, to pass in the modified selection. + // In this case, passing in an empty [UserManagedSelectionModel] creates a + // no selection model to clear all selection when rebuilt. + setState(() { + _myState.selectionModels[charts.SelectionModelType.info] = + new charts.UserManagedSelectionModel(); + }); + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/slider.dart b/example/lib/behaviors/slider.dart new file mode 100644 index 0000000..df45c6b --- /dev/null +++ b/example/lib/behaviors/slider.dart @@ -0,0 +1,196 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; + +/// This is just a simple line chart with a behavior that adds slider controls. +/// +/// A [Slider] behavior is added manually to enable slider controls, with an +/// initial position at 1 along the domain axis. +/// +/// An onChange event handler has been configured to demonstrate updating a div +/// with data from the slider's current position. An "initial" drag state event +/// will be fired when the chart is drawn because an initial domain value is +/// set. +/// +/// [Slider.moveSliderToDomain] can be called to programmatically position the +/// slider. This is useful for synchronizing the slider with external elements. +class SliderLine extends StatefulWidget { + final List> seriesList; + final bool animate; + + SliderLine(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory SliderLine.withSampleData() { + return new SliderLine( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SliderLine.withRandomData() { + return new SliderLine(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + // We need a Stateful widget to build the selection details with the current + // selection as the state. + @override + State createState() => new _SliderCallbackState(); + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +class _SliderCallbackState extends State { + num? _sliderDomainValue; + String? _sliderDragState; + Point? _sliderPosition; + + // Handles callbacks when the user drags the slider. + _onSliderChange(Point point, dynamic domain, String roleId, + charts.SliderListenerDragState dragState) { + // Request a build. + void rebuild(_) { + setState(() { + _sliderDomainValue = (domain * 10).round() / 10; + _sliderDragState = dragState.toString(); + _sliderPosition = point; + }); + } + + SchedulerBinding.instance!.addPostFrameCallback(rebuild); + } + + @override + Widget build(BuildContext context) { + // The children consist of a Chart and Text widgets below to hold the info. + final children = [ + new SizedBox( + height: 150.0, + child: new charts.LineChart( + widget.seriesList, + animate: widget.animate, + // Configures a [Slider] behavior. + // + // Available options include: + // + // [eventTrigger] configures the type of mouse gesture that controls + // 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] takes in a [SliderStyle] configuration object, and + // configures the color and sizing of the slider line and handle. + behaviors: [ + new charts.Slider( + initialDomainValue: 1.0, onChangeCallback: _onSliderChange), + ], + )), + ]; + + // If there is a slider change event, then include the details. + if (_sliderDomainValue != null) { + children.add(new Padding( + padding: new EdgeInsets.only(top: 5.0), + child: new Text('Slider domain value: ${_sliderDomainValue}'))); + } + if (_sliderPosition != null) { + children.add(new Padding( + padding: new EdgeInsets.only(top: 5.0), + child: new Text( + 'Slider position: ${_sliderPosition!.x}, ${_sliderPosition!.y}'))); + } + if (_sliderDragState != null) { + children.add(new Padding( + padding: new EdgeInsets.only(top: 5.0), + child: new Text('Slider drag state: ${_sliderDragState}'))); + } + + return new Column(children: children); + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/behaviors/sliding_viewport_on_selection.dart b/example/lib/behaviors/sliding_viewport_on_selection.dart new file mode 100644 index 0000000..679c73a --- /dev/null +++ b/example/lib/behaviors/sliding_viewport_on_selection.dart @@ -0,0 +1,144 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of the chart behavior that centers the viewport on domain selection. + +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SlidingViewportOnSelection extends StatelessWidget { + final List> seriesList; + final bool animate; + + SlidingViewportOnSelection(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory SlidingViewportOnSelection.withSampleData() { + return new SlidingViewportOnSelection( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SlidingViewportOnSelection.withRandomData() { + return new SlidingViewportOnSelection(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + new OrdinalSales('2018', random.nextInt(100)), + new OrdinalSales('2019', random.nextInt(100)), + new OrdinalSales('2020', random.nextInt(100)), + new OrdinalSales('2021', random.nextInt(100)), + new OrdinalSales('2022', random.nextInt(100)), + new OrdinalSales('2023', random.nextInt(100)), + new OrdinalSales('2024', random.nextInt(100)), + new OrdinalSales('2025', random.nextInt(100)), + new OrdinalSales('2026', random.nextInt(100)), + new OrdinalSales('2027', random.nextInt(100)), + new OrdinalSales('2028', random.nextInt(100)), + new OrdinalSales('2029', random.nextInt(100)), + new OrdinalSales('2030', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + behaviors: [ + // Add the sliding viewport behavior to have the viewport center on the + // domain that is currently selected. + new charts.SlidingViewport(), + // A pan and zoom behavior helps demonstrate the sliding viewport + // behavior by allowing the data visible in the viewport to be adjusted + // dynamically. + new charts.PanAndZoomBehavior(), + ], + // Set an initial viewport to demonstrate the sliding viewport behavior on + // initial chart load. + domainAxis: new charts.OrdinalAxisSpec( + viewport: new charts.OrdinalViewport('2018', 4)), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + new OrdinalSales('2018', 33), + new OrdinalSales('2019', 80), + new OrdinalSales('2020', 21), + new OrdinalSales('2021', 77), + new OrdinalSales('2022', 8), + new OrdinalSales('2023', 12), + new OrdinalSales('2024', 42), + new OrdinalSales('2025', 70), + new OrdinalSales('2026', 77), + new OrdinalSales('2027', 55), + new OrdinalSales('2028', 19), + new OrdinalSales('2029', 66), + new OrdinalSales('2030', 27), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/combo_chart/combo_gallery.dart b/example/lib/combo_chart/combo_gallery.dart new file mode 100644 index 0000000..e566551 --- /dev/null +++ b/example/lib/combo_chart/combo_gallery.dart @@ -0,0 +1,57 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'date_time_line_point.dart'; +import 'numeric_line_bar.dart'; +import 'numeric_line_point.dart'; +import 'ordinal_bar_line.dart'; +import 'scatter_plot_line.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Ordinal Combo Chart', + subtitle: 'Ordinal combo chart with bars and lines', + childBuilder: () => new OrdinalComboBarLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Numeric Line Bar Combo Chart', + subtitle: 'Numeric combo chart with lines and bars', + childBuilder: () => new NumericComboLineBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Numeric Line Points Combo Chart', + subtitle: 'Numeric combo chart with lines and points', + childBuilder: () => new NumericComboLinePointChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Time Series Combo Chart', + subtitle: 'Time series combo chart with lines and points', + childBuilder: () => new DateTimeComboLinePointChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Scatter Plot Combo Chart', + subtitle: 'Scatter plot combo chart with a line', + childBuilder: () => new ScatterPlotComboLineChart.withRandomData(), + ), + ]; +} diff --git a/example/lib/combo_chart/date_time_line_point.dart b/example/lib/combo_chart/date_time_line_point.dart new file mode 100644 index 0000000..a279588 --- /dev/null +++ b/example/lib/combo_chart/date_time_line_point.dart @@ -0,0 +1,183 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a combo time series chart with two series rendered as lines, and +/// a third rendered as points along the top line with a different color. +/// +/// This example demonstrates a method for drawing points along a line using a +/// different color from the main series color. The line renderer supports +/// drawing points with the "includePoints" option, but those points will share +/// the same color as the line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class DateTimeComboLinePointChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DateTimeComboLinePointChart(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory DateTimeComboLinePointChart.withSampleData() { + return new DateTimeComboLinePointChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DateTimeComboLinePointChart.withRandomData() { + return new DateTimeComboLinePointChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + final tableSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + final mobileSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), tableSalesData[0].sales), + new TimeSeriesSales(new DateTime(2017, 9, 26), tableSalesData[1].sales), + new TimeSeriesSales(new DateTime(2017, 10, 3), tableSalesData[2].sales), + new TimeSeriesSales(new DateTime(2017, 10, 10), tableSalesData[3].sales), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom point renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customPoint'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Configure the default renderer as a line renderer. This will be used + // for any series that does not define a rendererIdKey. + // + // This is the default configuration, but is shown here for illustration. + defaultRenderer: new charts.LineRendererConfig(), + // Custom renderer configuration for the point series. + customSeriesRenderers: [ + new charts.PointRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customPoint') + ], + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final desktopSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + final tableSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 10), + new TimeSeriesSales(new DateTime(2017, 9, 26), 50), + new TimeSeriesSales(new DateTime(2017, 10, 3), 200), + new TimeSeriesSales(new DateTime(2017, 10, 10), 150), + ]; + + final mobileSalesData = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 10), + new TimeSeriesSales(new DateTime(2017, 9, 26), 50), + new TimeSeriesSales(new DateTime(2017, 10, 3), 200), + new TimeSeriesSales(new DateTime(2017, 10, 10), 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom point renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customPoint'), + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/example/lib/combo_chart/numeric_line_bar.dart b/example/lib/combo_chart/numeric_line_bar.dart new file mode 100644 index 0000000..4dcbf36 --- /dev/null +++ b/example/lib/combo_chart/numeric_line_bar.dart @@ -0,0 +1,174 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a numeric combo chart with two series rendered as bars, and a +/// third rendered as a line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class NumericComboLineBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + NumericComboLineBarChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory NumericComboLineBarChart.withSampleData() { + return new NumericComboLineBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory NumericComboLineBarChart.withRandomData() { + return new NumericComboLineBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + final tableSalesData = [ + new LinearSales(0, desktopSalesData[0].sales), + new LinearSales(1, desktopSalesData[1].sales), + new LinearSales(2, desktopSalesData[2].sales), + new LinearSales(3, desktopSalesData[3].sales), + ]; + + final mobileSalesData = [ + new LinearSales(0, tableSalesData[0].sales * 2), + new LinearSales(1, tableSalesData[1].sales * 2), + new LinearSales(2, tableSalesData[2].sales * 2), + new LinearSales(3, tableSalesData[3].sales * 2), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: desktopSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: tableSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: mobileSalesData), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.NumericComboChart(seriesList, + animate: animate, + // Configure the default renderer as a line renderer. This will be used + // for any series that does not define a rendererIdKey. + defaultRenderer: new charts.LineRendererConfig(), + // Custom renderer configuration for the bar series. + customSeriesRenderers: [ + new charts.BarRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customBar') + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final desktopSalesData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + final tableSalesData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + final mobileSalesData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: desktopSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: tableSalesData, + ) + // Configure our custom bar renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customBar'), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: mobileSalesData), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/combo_chart/numeric_line_point.dart b/example/lib/combo_chart/numeric_line_point.dart new file mode 100644 index 0000000..31ae957 --- /dev/null +++ b/example/lib/combo_chart/numeric_line_point.dart @@ -0,0 +1,175 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a numeric combo chart with two series rendered as lines, and a +/// third rendered as points along the top line with a different color. +/// +/// This example demonstrates a method for drawing points along a line using a +/// different color from the main series color. The line renderer supports +/// drawing points with the "includePoints" option, but those points will share +/// the same color as the line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class NumericComboLinePointChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + NumericComboLinePointChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory NumericComboLinePointChart.withSampleData() { + return new NumericComboLinePointChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory NumericComboLinePointChart.withRandomData() { + return new NumericComboLinePointChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + final tableSalesData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + final mobileSalesData = [ + new LinearSales(0, tableSalesData[0].sales), + new LinearSales(1, tableSalesData[1].sales), + new LinearSales(2, tableSalesData[2].sales), + new LinearSales(3, tableSalesData[3].sales), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom point renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customPoint'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.NumericComboChart(seriesList, + animate: animate, + // Configure the default renderer as a line renderer. This will be used + // for any series that does not define a rendererIdKey. + defaultRenderer: new charts.LineRendererConfig(), + // Custom renderer configuration for the point series. + customSeriesRenderers: [ + new charts.PointRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customPoint') + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final desktopSalesData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + final tableSalesData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + final mobileSalesData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: tableSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom point renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customPoint'), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/combo_chart/ordinal_bar_line.dart b/example/lib/combo_chart/ordinal_bar_line.dart new file mode 100644 index 0000000..d9390f0 --- /dev/null +++ b/example/lib/combo_chart/ordinal_bar_line.dart @@ -0,0 +1,166 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of an ordinal combo chart with two series rendered as bars, and a +/// third rendered as a line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class OrdinalComboBarLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + OrdinalComboBarLineChart(this.seriesList, {this.animate = false}); + + factory OrdinalComboBarLineChart.withSampleData() { + return new OrdinalComboBarLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory OrdinalComboBarLineChart.withRandomData() { + return new OrdinalComboBarLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom line renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.OrdinalComboChart(seriesList, + animate: animate, + // Configure the default renderer as a bar renderer. + defaultRenderer: new charts.BarRendererConfig( + groupingType: charts.BarGroupingType.grouped), + // Custom renderer configuration for the line series. This will be used for + // any series that does not define a rendererIdKey. + customSeriesRenderers: [ + new charts.LineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customLine') + ]); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tableSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 200), + new OrdinalSales('2017', 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tableSalesData), + new charts.Series( + id: 'Mobile ', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData) + // Configure our custom line renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customLine'), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/combo_chart/scatter_plot_line.dart b/example/lib/combo_chart/scatter_plot_line.dart new file mode 100644 index 0000000..54ff376 --- /dev/null +++ b/example/lib/combo_chart/scatter_plot_line.dart @@ -0,0 +1,198 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a combo scatter plot chart with a second series rendered as a +/// line. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class ScatterPlotComboLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + ScatterPlotComboLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory ScatterPlotComboLineChart.withSampleData() { + return new ScatterPlotComboLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ScatterPlotComboLineChart.withRandomData() { + return new ScatterPlotComboLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + final desktopSalesData = [ + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + ]; + + var myRegressionData = [ + new LinearSales(0, desktopSalesData[0].sales, 3.5), + new LinearSales( + 100, desktopSalesData[desktopSalesData.length - 1].sales, 7.5), + ]; + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + // Providing a radius function is optional. + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: desktopSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myRegressionData) + // Configure our custom line renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customLine'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + // Configure the default renderer as a point renderer. This will be used + // for any series that does not define a rendererIdKey. + // + // This is the default configuration, but is shown here for + // illustration. + defaultRenderer: new charts.PointRendererConfig(), + // Custom renderer configuration for the line series. + customSeriesRenderers: [ + new charts.LineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customLine', + // Configure the regression line to be painted above the points. + // + // By default, series drawn by the point renderer are painted on + // top of those drawn by a line renderer. + layoutPaintOrder: charts.LayoutViewPaintOrder.point + 1) + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final desktopSalesData = [ + new LinearSales(0, 5, 3.0), + new LinearSales(10, 25, 5.0), + new LinearSales(12, 75, 4.0), + new LinearSales(13, 225, 5.0), + new LinearSales(16, 50, 4.0), + new LinearSales(24, 75, 3.0), + new LinearSales(25, 100, 3.0), + new LinearSales(34, 150, 5.0), + new LinearSales(37, 10, 4.5), + new LinearSales(45, 300, 8.0), + new LinearSales(52, 15, 4.0), + new LinearSales(56, 200, 7.0), + ]; + + var myRegressionData = [ + new LinearSales(0, 5, 3.5), + new LinearSales(56, 240, 3.5), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + // Providing a radius function is optional. + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: desktopSalesData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myRegressionData) + // Configure our custom line renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customLine'), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + + LinearSales(this.year, this.sales, this.radius); +} diff --git a/example/lib/drawer.dart b/example/lib/drawer.dart new file mode 100644 index 0000000..9d85a8f --- /dev/null +++ b/example/lib/drawer.dart @@ -0,0 +1,51 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +/// A menu drawer supporting toggling theme and performance overlay. +class GalleryDrawer extends StatelessWidget { + final bool showPerformanceOverlay; + final ValueChanged onShowPerformanceOverlayChanged; + + GalleryDrawer( + {Key? key, + this.showPerformanceOverlay = false, + required this.onShowPerformanceOverlayChanged}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return new Drawer( + child: new ListView(children: [ + // Performance overlay toggle. + new ListTile( + leading: new Icon(Icons.assessment), + title: new Text('Performance Overlay'), + onTap: () { + onShowPerformanceOverlayChanged(!showPerformanceOverlay); + }, + selected: showPerformanceOverlay, + trailing: new Checkbox( + value: showPerformanceOverlay, + onChanged: (bool? value) { + onShowPerformanceOverlayChanged(!showPerformanceOverlay); + }, + ), + ), + ]), + ); + } +} diff --git a/example/lib/gallery_scaffold.dart b/example/lib/gallery_scaffold.dart new file mode 100644 index 0000000..2a13c55 --- /dev/null +++ b/example/lib/gallery_scaffold.dart @@ -0,0 +1,66 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +typedef Widget GalleryWidgetBuilder(); + +/// Helper to build gallery. +class GalleryScaffold extends StatefulWidget { + /// The widget used for leading in a [ListTile]. + final Widget listTileIcon; + final String title; + final String subtitle; + final GalleryWidgetBuilder childBuilder; + + GalleryScaffold({ + required this.listTileIcon, + required this.title, + required this.subtitle, + required this.childBuilder, + }); + + /// Gets the gallery + Widget buildGalleryListTile(BuildContext context) => new ListTile( + leading: listTileIcon, + title: new Text(title), + subtitle: new Text(subtitle), + onTap: () { + Navigator.push(context, new MaterialPageRoute(builder: (_) => this)); + }); + + @override + _GalleryScaffoldState createState() => new _GalleryScaffoldState(); +} + +class _GalleryScaffoldState extends State { + void _handleButtonPress() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return new Scaffold( + appBar: new AppBar(title: new Text(widget.title)), + body: new Padding( + padding: const EdgeInsets.all(8.0), + child: new Column(children: [ + new SizedBox(height: 250.0, child: widget.childBuilder()), + ])), + floatingActionButton: new FloatingActionButton( + onPressed: _handleButtonPress, child: new Icon(Icons.refresh)), + ); + } +} diff --git a/example/lib/home.dart b/example/lib/home.dart new file mode 100644 index 0000000..3d96b96 --- /dev/null +++ b/example/lib/home.dart @@ -0,0 +1,127 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; +import 'dart:developer'; +import 'app_config.dart'; +import 'drawer.dart'; +import 'a11y/a11y_gallery.dart' as a11y show buildGallery; +import 'bar_chart/bar_gallery.dart' as bar show buildGallery; +import 'time_series_chart/time_series_gallery.dart' as time_series + show buildGallery; +import 'line_chart/line_gallery.dart' as line show buildGallery; +import 'scatter_plot_chart/scatter_plot_gallery.dart' as scatter_plot + show buildGallery; +import 'combo_chart/combo_gallery.dart' as combo show buildGallery; +import 'pie_chart/pie_gallery.dart' as pie show buildGallery; +import 'axes/axes_gallery.dart' as axes show buildGallery; +import 'behaviors/behaviors_gallery.dart' as behaviors show buildGallery; +import 'i18n/i18n_gallery.dart' as i18n show buildGallery; +import 'legends/legends_gallery.dart' as legends show buildGallery; + +/// Main entry point of the gallery app. +/// +/// This renders a list of all available demos. +class Home extends StatelessWidget { + final bool showPerformanceOverlay; + final ValueChanged onShowPerformanceOverlayChanged; + final a11yGalleries = a11y.buildGallery(); + final barGalleries = bar.buildGallery(); + final timeSeriesGalleries = time_series.buildGallery(); + final lineGalleries = line.buildGallery(); + final scatterPlotGalleries = scatter_plot.buildGallery(); + final comboGalleries = combo.buildGallery(); + final pieGalleries = pie.buildGallery(); + final axesGalleries = axes.buildGallery(); + final behaviorsGalleries = behaviors.buildGallery(); + final i18nGalleries = i18n.buildGallery(); + final legendsGalleries = legends.buildGallery(); + + Home( + {Key? key, + this.showPerformanceOverlay = false, + required this.onShowPerformanceOverlayChanged}) + : super(key: key) { + assert(onShowPerformanceOverlayChanged != null); + } + + @override + Widget build(BuildContext context) { + var galleries = []; + + galleries.addAll( + a11yGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example bar charts. + galleries.addAll( + barGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example time series charts. + galleries.addAll(timeSeriesGalleries + .map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example line charts. + galleries.addAll( + lineGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example scatter plot charts. + galleries.addAll(scatterPlotGalleries + .map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example pie charts. + galleries.addAll( + comboGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example pie charts. + galleries.addAll( + pieGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + // Add example custom axis. + galleries.addAll( + axesGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + galleries.addAll(behaviorsGalleries + .map((gallery) => gallery.buildGalleryListTile(context))); + + // Add legends examples + galleries.addAll(legendsGalleries + .map((gallery) => gallery.buildGalleryListTile(context))); + + // Add examples for i18n. + galleries.addAll( + i18nGalleries.map((gallery) => gallery.buildGalleryListTile(context))); + + _setupPerformance(); + + return new Scaffold( + drawer: new GalleryDrawer( + showPerformanceOverlay: showPerformanceOverlay, + onShowPerformanceOverlayChanged: onShowPerformanceOverlayChanged), + appBar: new AppBar(title: new Text(defaultConfig.appName)), + body: new ListView(padding: kMaterialListPadding, children: galleries), + ); + } + + void _setupPerformance() { + // Change [printPerformance] to true and set the app to release mode to + // print performance numbers to console. By default, Flutter builds in debug + // mode and this mode is slow. To build in release mode, specify the flag + // blaze-run flag "--define flutter_build_mode=release". + // The build target must also be an actual device and not the emulator. + charts.Performance.time = (String tag) => Timeline.startSync(tag); + charts.Performance.timeEnd = (_) => Timeline.finishSync(); + } +} diff --git a/example/lib/i18n/i18n_gallery.dart b/example/lib/i18n/i18n_gallery.dart new file mode 100644 index 0000000..cab9066 --- /dev/null +++ b/example/lib/i18n/i18n_gallery.dart @@ -0,0 +1,50 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'rtl_bar_chart.dart'; +import 'rtl_line_chart.dart'; +import 'rtl_line_segments.dart'; +import 'rtl_series_legend.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'RTL Bar Chart', + subtitle: 'Simple bar chart in RTL', + childBuilder: () => new RTLBarChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'RTL Line Chart', + subtitle: 'Simple line chart in RTL', + childBuilder: () => new RTLLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'RTL Line Segments', + subtitle: 'Stacked area chart with style segments in RTL', + childBuilder: () => new RTLLineSegments.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.flag), + title: 'RTL Series Legend', + subtitle: 'Series legend in RTL', + childBuilder: () => new RTLSeriesLegend.withRandomData(), + ), + ]; +} diff --git a/example/lib/i18n/rtl_bar_chart.dart b/example/lib/i18n/rtl_bar_chart.dart new file mode 100644 index 0000000..50637d8 --- /dev/null +++ b/example/lib/i18n/rtl_bar_chart.dart @@ -0,0 +1,119 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// RTL Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class RTLBarChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + RTLBarChart(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory RTLBarChart.withSampleData() { + return new RTLBarChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory RTLBarChart.withRandomData() { + return new RTLBarChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Charts will determine if RTL is enabled by checking the directionality by + // requesting Directionality.of(context). This returns the text direction + // from the closest instance of that encloses the context passed to build + // the chart. A [TextDirection.rtl] will be treated as a RTL chart. This + // means that the directionality widget does not have to directly wrap each + // chart. It is show here as an example only. + // + // By default, when a chart detects RTL: + // Measure axis positions are flipped. Primary measure axis is on the right + // and the secondary measure axis is on the left (when used). + // Domain axis' first domain starts on the right and grows left. + // + // Optionally, [RTLSpec] can be passed in when creating the chart to specify + // chart display settings in RTL mode. + return new Directionality( + textDirection: TextDirection.rtl, + child: new charts.BarChart( + seriesList, + animate: animate, + vertical: false, + )); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/i18n/rtl_line_chart.dart b/example/lib/i18n/rtl_line_chart.dart new file mode 100644 index 0000000..7bc4e9e --- /dev/null +++ b/example/lib/i18n/rtl_line_chart.dart @@ -0,0 +1,115 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// RTL Line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class RTLLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + RTLLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory RTLLineChart.withSampleData() { + return new RTLLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory RTLLineChart.withRandomData() { + return new RTLLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Charts will determine if RTL is enabled by checking the directionality by + // requesting Directionality.of(context). This returns the text direction + // from the closest instance of that encloses the context passed to build + // the chart. A [TextDirection.rtl] will be treated as a RTL chart. This + // means that the directionality widget does not have to directly wrap each + // chart. It is show here as an example only. + // + // By default, when a chart detects RTL: + // Measure axis positions are flipped. Primary measure axis is on the right + // and the secondary measure axis is on the left (when used). + // Domain axis' first domain starts on the right and grows left. + return new Directionality( + textDirection: TextDirection.rtl, + child: new charts.LineChart( + seriesList, + animate: animate, + )); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/i18n/rtl_line_segments.dart b/example/lib/i18n/rtl_line_segments.dart new file mode 100644 index 0000000..2e2af9e --- /dev/null +++ b/example/lib/i18n/rtl_line_segments.dart @@ -0,0 +1,248 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a RTL stacked area chart with changing styles within each line. +/// +/// Each series of data in this example contains different values for color, +/// dashPattern, or strokeWidthPx between each datum. The line and area skirt +/// will be rendered in segments, with the styling of the series changing when +/// these data attributes change. +/// +/// Note that if a dashPattern or strokeWidth value is not found for a +/// particular datum, then the chart will fall back to use the value defined in +/// the [charts.LineRendererConfig]. This could be used, for example, to define +/// a default dash pattern for the series, with only a specific datum called out +/// with a different pattern. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class RTLLineSegments extends StatelessWidget { + final List> seriesList; + final bool animate; + + RTLLineSegments(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory RTLLineSegments.withSampleData() { + return new RTLLineSegments( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory RTLLineSegments.withRandomData() { + return new RTLLineSegments(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + // Series of data with static dash pattern and stroke width. The colorFn + // accessor will colorize each datum (for all three series). + final colorChangeData = [ + new LinearSales(0, random.nextInt(100), null, 2.0), + new LinearSales(1, random.nextInt(100), null, 2.0), + new LinearSales(2, random.nextInt(100), null, 2.0), + new LinearSales(3, random.nextInt(100), null, 2.0), + new LinearSales(4, random.nextInt(100), null, 2.0), + new LinearSales(5, random.nextInt(100), null, 2.0), + new LinearSales(6, random.nextInt(100), null, 2.0), + ]; + + // Series of data with changing color and dash pattern. + final dashPatternChangeData = [ + new LinearSales(0, random.nextInt(100), [2, 2], 2.0), + new LinearSales(1, random.nextInt(100), [2, 2], 2.0), + new LinearSales(2, random.nextInt(100), [4, 4], 2.0), + new LinearSales(3, random.nextInt(100), [4, 4], 2.0), + new LinearSales(4, random.nextInt(100), [4, 4], 2.0), + new LinearSales(5, random.nextInt(100), [8, 3, 2, 3], 2.0), + new LinearSales(6, random.nextInt(100), [8, 3, 2, 3], 2.0), + ]; + + // Series of data with changing color and stroke width. + final strokeWidthChangeData = [ + new LinearSales(0, random.nextInt(100), null, 2.0), + new LinearSales(1, random.nextInt(100), null, 2.0), + new LinearSales(2, random.nextInt(100), null, 4.0), + new LinearSales(3, random.nextInt(100), null, 4.0), + new LinearSales(4, random.nextInt(100), null, 4.0), + new LinearSales(5, random.nextInt(100), null, 6.0), + new LinearSales(6, random.nextInt(100), null, 6.0), + ]; + + // Generate 2 shades of each color so that we can style the line segments. + final blue = charts.MaterialPalette.blue.makeShades(2); + final red = charts.MaterialPalette.red.makeShades(2); + final green = charts.MaterialPalette.green.makeShades(2); + + return [ + new charts.Series( + id: 'Color Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? blue[1] : blue[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: colorChangeData, + ), + new charts.Series( + id: 'Dash Pattern Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? red[1] : red[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: dashPatternChangeData, + ), + new charts.Series( + id: 'Stroke Width Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? green[1] : green[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: strokeWidthChangeData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Charts will determine if RTL is enabled by checking the directionality by + // requesting Directionality.of(context). This returns the text direction + // from the closest instance of that encloses the context passed to build + // the chart. A [TextDirection.rtl] will be treated as a RTL chart. This + // means that the directionality widget does not have to directly wrap each + // chart. It is show here as an example only. + // + // By default, when a chart detects RTL: + // Measure axis positions are flipped. Primary measure axis is on the right + // and the secondary measure axis is on the left (when used). + // Domain axis' first domain starts on the right and grows left. + return new Directionality( + textDirection: TextDirection.rtl, + child: new charts.LineChart( + seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate, + )); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + // Series of data with static dash pattern and stroke width. The colorFn + // accessor will colorize each datum (for all three series). + final colorChangeData = [ + new LinearSales(0, 5, null, 2.0), + new LinearSales(1, 15, null, 2.0), + new LinearSales(2, 25, null, 2.0), + new LinearSales(3, 75, null, 2.0), + new LinearSales(4, 100, null, 2.0), + new LinearSales(5, 90, null, 2.0), + new LinearSales(6, 75, null, 2.0), + ]; + + // Series of data with changing color and dash pattern. + final dashPatternChangeData = [ + new LinearSales(0, 5, [2, 2], 2.0), + new LinearSales(1, 15, [2, 2], 2.0), + new LinearSales(2, 25, [4, 4], 2.0), + new LinearSales(3, 75, [4, 4], 2.0), + new LinearSales(4, 100, [4, 4], 2.0), + new LinearSales(5, 90, [8, 3, 2, 3], 2.0), + new LinearSales(6, 75, [8, 3, 2, 3], 2.0), + ]; + + // Series of data with changing color and stroke width. + final strokeWidthChangeData = [ + new LinearSales(0, 5, null, 2.0), + new LinearSales(1, 15, null, 2.0), + new LinearSales(2, 25, null, 4.0), + new LinearSales(3, 75, null, 4.0), + new LinearSales(4, 100, null, 4.0), + new LinearSales(5, 90, null, 6.0), + new LinearSales(6, 75, null, 6.0), + ]; + + // Generate 2 shades of each color so that we can style the line segments. + final blue = charts.MaterialPalette.blue.makeShades(2); + final red = charts.MaterialPalette.red.makeShades(2); + final green = charts.MaterialPalette.green.makeShades(2); + + return [ + new charts.Series( + id: 'Color Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? blue[1] : blue[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: colorChangeData, + ), + new charts.Series( + id: 'Dash Pattern Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? red[1] : red[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: dashPatternChangeData, + ), + new charts.Series( + id: 'Stroke Width Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? green[1] : green[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: strokeWidthChangeData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final List? dashPattern; + final double strokeWidthPx; + + LinearSales(this.year, this.sales, this.dashPattern, this.strokeWidthPx); +} diff --git a/example/lib/i18n/rtl_series_legend.dart b/example/lib/i18n/rtl_series_legend.dart new file mode 100644 index 0000000..a4006b9 --- /dev/null +++ b/example/lib/i18n/rtl_series_legend.dart @@ -0,0 +1,206 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// RTL Bar chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class RTLSeriesLegend extends StatelessWidget { + final List> seriesList; + final bool animate; + + RTLSeriesLegend(this.seriesList, {this.animate = false}); + + /// Creates a [BarChart] with sample data and no transition. + factory RTLSeriesLegend.withSampleData() { + return new RTLSeriesLegend( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory RTLSeriesLegend.withRandomData() { + return new RTLSeriesLegend(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Charts will determine if RTL is enabled by checking the directionality by + // requesting Directionality.of(context). This returns the text direction + // from the closest instance of that encloses the context passed to build + // the chart. A [TextDirection.rtl] will be treated as a RTL chart. This + // means that the directionality widget does not have to directly wrap each + // chart. It is show here as an example only. + // + // When the legend behavior detects RTL: + // [BehaviorPosition.start] is to the right of the chart. + // [BehaviorPosition.end] is to the left of the chart. + // + // If the [BehaviorPosition] is top or bottom, the start justification + // is to the right, and the end justification is to the left. + // + // The legend's tabular layout will also layout rows and columns from right + // to left. + // + // The below example changes the position to 'start' and max rows of 2 in + // order to show these effects, but are not required for SeriesLegend to + // work with the correct directionality. + return new Directionality( + textDirection: TextDirection.rtl, + child: new charts.BarChart( + seriesList, + animate: animate, + behaviors: [ + new charts.SeriesLegend( + position: charts.BehaviorPosition.end, desiredMaxRows: 2) + ], + )); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/legends/datum_legend_options.dart b/example/lib/legends/datum_legend_options.dart new file mode 100644 index 0000000..54aee0e --- /dev/null +++ b/example/lib/legends/datum_legend_options.dart @@ -0,0 +1,136 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Pie chart with example of a legend with customized position, justification, +/// desired max rows, padding, and entry text styles. These options are shown as +/// an example of how to use the customizations, they do not necessary have to +/// be used together in this way. Choosing [end] as the position does not +/// require the justification to also be [endDrawArea]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class DatumLegendOptions extends StatelessWidget { + final List> seriesList; + final bool animate; + + DatumLegendOptions(this.seriesList, {this.animate = false}); + + factory DatumLegendOptions.withSampleData() { + return new DatumLegendOptions( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DatumLegendOptions.withRandomData() { + return new DatumLegendOptions(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart( + seriesList, + animate: animate, + // Add the legend behavior to the chart to turn on legends. + // This example shows how to change the position and justification of + // the legend, in addition to altering the max rows and padding. + behaviors: [ + new charts.DatumLegend( + // Positions for "start" and "end" will be left and right respectively + // for widgets with a build context that has directionality ltr. + // For rtl, "start" and "end" will be right and left respectively. + // Since this example has directionality of ltr, the legend is + // positioned on the right side of the chart. + position: charts.BehaviorPosition.end, + // For a legend that is positioned on the left or right of the chart, + // setting the justification for [endDrawArea] is aligned to the + // bottom of the chart draw area. + outsideJustification: charts.OutsideJustification.endDrawArea, + // By default, if the position of the chart is on the left or right of + // the chart, [horizontalFirst] is set to false. This means that the + // legend entries will grow as new rows first instead of a new column. + horizontalFirst: false, + // By setting this value to 2, the legend entries will grow up to two + // rows before adding a new column. + desiredMaxRows: 2, + // This defines the padding around each legend entry. + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + // Render the legend entry text with custom styles. + entryTextStyle: charts.TextStyleSpec( + color: charts.MaterialPalette.purple.shadeDefault, + fontFamily: 'Georgia', + fontSize: 11), + ) + ], + ); + } + + /// Create series list with one series + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/legends/datum_legend_with_measures.dart b/example/lib/legends/datum_legend_with_measures.dart new file mode 100644 index 0000000..1b2ce31 --- /dev/null +++ b/example/lib/legends/datum_legend_with_measures.dart @@ -0,0 +1,146 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart with example of a legend with customized position, justification, +/// desired max rows, and padding. These options are shown as an example of how +/// to use the customizations, they do not necessary have to be used together in +/// this way. Choosing [end] as the position does not require the justification +/// to also be [endDrawArea]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example that shows how to build a datum legend that shows measure values. +/// +/// Also shows the option to provide a custom measure formatter. +class DatumLegendWithMeasures extends StatelessWidget { + final List> seriesList; + final bool animate; + + DatumLegendWithMeasures(this.seriesList, {this.animate = false}); + + factory DatumLegendWithMeasures.withSampleData() { + return new DatumLegendWithMeasures( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DatumLegendWithMeasures.withRandomData() { + return new DatumLegendWithMeasures(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(2014, random.nextInt(100)), + new LinearSales(2015, random.nextInt(100)), + new LinearSales(2016, random.nextInt(100)), + new LinearSales(2017, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart( + seriesList, + animate: animate, + // Add the legend behavior to the chart to turn on legends. + // This example shows how to optionally show measure and provide a custom + // formatter. + behaviors: [ + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // This is added in order to generate the image for the gallery to show + // an initial selection so that measure values are shown in the gallery. + new charts.InitialSelection( + selectedDataConfig: [ + new charts.SeriesDatumConfig('Sales', 0), + ], + ), + // EXCLUDE_FROM_GALLERY_DOCS_END + new charts.DatumLegend( + // Positions for "start" and "end" will be left and right respectively + // for widgets with a build context that has directionality ltr. + // For rtl, "start" and "end" will be right and left respectively. + // Since this example has directionality of ltr, the legend is + // positioned on the right side of the chart. + position: charts.BehaviorPosition.end, + // By default, if the position of the chart is on the left or right of + // the chart, [horizontalFirst] is set to false. This means that the + // legend entries will grow as new rows first instead of a new column. + horizontalFirst: false, + // This defines the padding around each legend entry. + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + // Set [showMeasures] to true to display measures in series legend. + showMeasures: true, + // Configure the measure value to be shown by default in the legend. + legendDefaultMeasure: charts.LegendDefaultMeasure.firstValue, + // Optionally provide a measure formatter to format the measure value. + // If none is specified the value is formatted as a decimal. + measureFormatter: (num? value) { + return value == null ? '-' : '${value}k'; + }, + ), + ], + ); + } + + /// Create series list with one series + static List> _createSampleData() { + final data = [ + new LinearSales(2014, 100), + new LinearSales(2015, 75), + new LinearSales(2016, 25), + new LinearSales(2017, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/legends/default_hidden_series_legend.dart b/example/lib/legends/default_hidden_series_legend.dart new file mode 100644 index 0000000..e1a4dcf --- /dev/null +++ b/example/lib/legends/default_hidden_series_legend.dart @@ -0,0 +1,188 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart with default hidden series legend example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class DefaultHiddenSeriesLegend extends StatelessWidget { + final List> seriesList; + final bool animate; + + DefaultHiddenSeriesLegend(this.seriesList, {this.animate = false}); + + factory DefaultHiddenSeriesLegend.withSampleData() { + return new DefaultHiddenSeriesLegend( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DefaultHiddenSeriesLegend.withRandomData() { + return new DefaultHiddenSeriesLegend(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the series legend behavior to the chart to turn on series legends. + // By default the legend will display above the chart. + behaviors: [ + new charts.SeriesLegend( + // Configures the "Other" series to be hidden on first chart draw. + defaultHiddenSeries: ['Other'], + ) + ], + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/legends/legend_custom_symbol.dart b/example/lib/legends/legend_custom_symbol.dart new file mode 100644 index 0000000..b5c7fcd --- /dev/null +++ b/example/lib/legends/legend_custom_symbol.dart @@ -0,0 +1,210 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart with custom symbol in legend example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example custom renderer that renders [IconData]. +/// +/// This is used to show that legend symbols can be assigned a custom symbol. +class IconRenderer extends charts.CustomSymbolRenderer { + final IconData iconData; + + IconRenderer(this.iconData); + + @override + Widget build(BuildContext context, + {Size? size, Color? color, bool enabled = true}) { + // Lighten the color if the symbol is not enabled + // Example: If user has tapped on a Series deselecting it. + if (color != null && !enabled) { + color = color.withOpacity(0.26); + } + + return new SizedBox.fromSize( + size: size, child: new Icon(iconData, color: color, size: 12.0)); + } +} + +class LegendWithCustomSymbol extends StatelessWidget { + final List> seriesList; + final bool animate; + + LegendWithCustomSymbol(this.seriesList, {this.animate = false}); + + factory LegendWithCustomSymbol.withSampleData() { + return new LegendWithCustomSymbol( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LegendWithCustomSymbol.withRandomData() { + return new LegendWithCustomSymbol(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the legend behavior to the chart to turn on legends. + // By default the legend will display above the chart. + // + // To change the symbol used in the legend, set the renderer attribute of + // symbolRendererKey to a SymbolRenderer. + behaviors: [new charts.SeriesLegend()], + defaultRenderer: new charts.BarRendererConfig( + symbolRenderer: new IconRenderer(Icons.cloud)), + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ) + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/legends/legends_gallery.dart b/example/lib/legends/legends_gallery.dart new file mode 100644 index 0000000..82be628 --- /dev/null +++ b/example/lib/legends/legends_gallery.dart @@ -0,0 +1,80 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'datum_legend_options.dart'; +import 'datum_legend_with_measures.dart'; +import 'default_hidden_series_legend.dart'; +import 'legend_custom_symbol.dart'; +import 'series_legend_options.dart'; +import 'series_legend_with_measures.dart'; +import 'simple_datum_legend.dart'; +import 'simple_series_legend.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Series Legend', + subtitle: 'A series legend for a bar chart with default settings', + childBuilder: () => new SimpleSeriesLegend.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Series Legend Options', + subtitle: + 'A series legend with custom positioning and spacing for a bar chart', + childBuilder: () => new LegendOptions.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Series Legend Custom Symbol', + subtitle: 'A series legend using a custom symbol renderer', + childBuilder: () => new LegendWithCustomSymbol.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Default Hidden Series Legend', + subtitle: 'A series legend showing a series hidden by default', + childBuilder: () => new DefaultHiddenSeriesLegend.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.insert_chart), + title: 'Series legend with measures', + subtitle: 'Series legend with measures and measure formatting', + childBuilder: () => new LegendWithMeasures.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Datum Legend', + subtitle: 'A datum legend for a pie chart with default settings', + childBuilder: () => new SimpleDatumLegend.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Datum Legend Options', + subtitle: + 'A datum legend with custom positioning and spacing for a pie chart', + childBuilder: () => new DatumLegendOptions.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Datum legend with measures', + subtitle: 'Datum legend with measures and measure formatting', + childBuilder: () => new DatumLegendWithMeasures.withRandomData(), + ), + ]; +} diff --git a/example/lib/legends/series_legend_options.dart b/example/lib/legends/series_legend_options.dart new file mode 100644 index 0000000..f5e32d4 --- /dev/null +++ b/example/lib/legends/series_legend_options.dart @@ -0,0 +1,215 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart with example of a legend with customized position, justification, +/// desired max rows, padding, and entry text styles. These options are shown as +/// an example of how to use the customizations, they do not necessary have to +/// be used together in this way. Choosing [end] as the position does not +/// require the justification to also be [endDrawArea]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class LegendOptions extends StatelessWidget { + final List> seriesList; + final bool animate; + + LegendOptions(this.seriesList, {this.animate = false}); + + factory LegendOptions.withSampleData() { + return new LegendOptions( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LegendOptions.withRandomData() { + return new LegendOptions(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the legend behavior to the chart to turn on legends. + // This example shows how to change the position and justification of + // the legend, in addition to altering the max rows and padding. + behaviors: [ + new charts.SeriesLegend( + // Positions for "start" and "end" will be left and right respectively + // for widgets with a build context that has directionality ltr. + // For rtl, "start" and "end" will be right and left respectively. + // Since this example has directionality of ltr, the legend is + // positioned on the right side of the chart. + position: charts.BehaviorPosition.end, + // For a legend that is positioned on the left or right of the chart, + // setting the justification for [endDrawArea] is aligned to the + // bottom of the chart draw area. + outsideJustification: charts.OutsideJustification.endDrawArea, + // By default, if the position of the chart is on the left or right of + // the chart, [horizontalFirst] is set to false. This means that the + // legend entries will grow as new rows first instead of a new column. + horizontalFirst: false, + // By setting this value to 2, the legend entries will grow up to two + // rows before adding a new column. + desiredMaxRows: 2, + // This defines the padding around each legend entry. + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + // Render the legend entry text with custom styles. + entryTextStyle: charts.TextStyleSpec( + color: charts.MaterialPalette.purple.shadeDefault, + fontFamily: 'Georgia', + fontSize: 11), + ) + ], + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/legends/series_legend_with_measures.dart b/example/lib/legends/series_legend_with_measures.dart new file mode 100644 index 0000000..21a7678 --- /dev/null +++ b/example/lib/legends/series_legend_with_measures.dart @@ -0,0 +1,228 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart with example of a legend with customized position, justification, +/// desired max rows, and padding. These options are shown as an example of how +/// to use the customizations, they do not necessary have to be used together in +/// this way. Choosing [end] as the position does not require the justification +/// to also be [endDrawArea]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +/// Example that shows how to build a series legend that shows measure values +/// when a datum is selected. +/// +/// Also shows the option to provide a custom measure formatter. +class LegendWithMeasures extends StatelessWidget { + final List> seriesList; + final bool animate; + + LegendWithMeasures(this.seriesList, {this.animate = false}); + + factory LegendWithMeasures.withSampleData() { + return new LegendWithMeasures( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LegendWithMeasures.withRandomData() { + return new LegendWithMeasures(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the legend behavior to the chart to turn on legends. + // This example shows how to optionally show measure and provide a custom + // formatter. + behaviors: [ + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // This is added in order to generate the image for the gallery to show + // an initial selection so that measure values are shown in the gallery. + new charts.InitialSelection( + selectedDataConfig: [ + new charts.SeriesDatumConfig('Desktop', '2016'), + new charts.SeriesDatumConfig('Tablet', '2016'), + new charts.SeriesDatumConfig('Mobile', '2016'), + new charts.SeriesDatumConfig('Other', '2016'), + ], + ), + // EXCLUDE_FROM_GALLERY_DOCS_END + new charts.SeriesLegend( + // Positions for "start" and "end" will be left and right respectively + // for widgets with a build context that has directionality ltr. + // For rtl, "start" and "end" will be right and left respectively. + // Since this example has directionality of ltr, the legend is + // positioned on the right side of the chart. + position: charts.BehaviorPosition.end, + // By default, if the position of the chart is on the left or right of + // the chart, [horizontalFirst] is set to false. This means that the + // legend entries will grow as new rows first instead of a new column. + horizontalFirst: false, + // This defines the padding around each legend entry. + cellPadding: new EdgeInsets.only(right: 4.0, bottom: 4.0), + // Set show measures to true to display measures in series legend, + // when the datum is selected. + showMeasures: true, + // Optionally provide a measure formatter to format the measure value. + // If none is specified the value is formatted as a decimal. + measureFormatter: (num? value) { + return value == null ? '-' : '${value}k'; + }, + ), + ], + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + // Purposely have a missing datum for 2016 to show the null measure format + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/legends/simple_datum_legend.dart b/example/lib/legends/simple_datum_legend.dart new file mode 100644 index 0000000..fe130a5 --- /dev/null +++ b/example/lib/legends/simple_datum_legend.dart @@ -0,0 +1,104 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart with series legend example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class SimpleDatumLegend extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleDatumLegend(this.seriesList, {this.animate = false}); + + factory SimpleDatumLegend.withSampleData() { + return new SimpleDatumLegend( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleDatumLegend.withRandomData() { + return new SimpleDatumLegend(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart( + seriesList, + animate: animate, + // Add the series legend behavior to the chart to turn on series legends. + // By default the legend will display above the chart. + behaviors: [new charts.DatumLegend()], + ); + } + + /// Create series list with one series + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/legends/simple_series_legend.dart b/example/lib/legends/simple_series_legend.dart new file mode 100644 index 0000000..1480793 --- /dev/null +++ b/example/lib/legends/simple_series_legend.dart @@ -0,0 +1,183 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Bar chart with series legend example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:flutter/material.dart'; +import 'package:charts_flutter/flutter.dart' as charts; + +class SimpleSeriesLegend extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleSeriesLegend(this.seriesList, {this.animate = false}); + + factory SimpleSeriesLegend.withSampleData() { + return new SimpleSeriesLegend( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleSeriesLegend.withRandomData() { + return new SimpleSeriesLegend(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final desktopSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', random.nextInt(100)), + new OrdinalSales('2015', random.nextInt(100)), + new OrdinalSales('2016', random.nextInt(100)), + new OrdinalSales('2017', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.BarChart( + seriesList, + animate: animate, + barGroupingType: charts.BarGroupingType.grouped, + // Add the series legend behavior to the chart to turn on series legends. + // By default the legend will display above the chart. + behaviors: [new charts.SeriesLegend()], + ); + } + + /// Create series list with multiple series + static List> _createSampleData() { + final desktopSalesData = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + final tabletSalesData = [ + new OrdinalSales('2014', 25), + new OrdinalSales('2015', 50), + new OrdinalSales('2016', 10), + new OrdinalSales('2017', 20), + ]; + + final mobileSalesData = [ + new OrdinalSales('2014', 10), + new OrdinalSales('2015', 15), + new OrdinalSales('2016', 50), + new OrdinalSales('2017', 45), + ]; + + final otherSalesData = [ + new OrdinalSales('2014', 20), + new OrdinalSales('2015', 35), + new OrdinalSales('2016', 15), + new OrdinalSales('2017', 10), + ]; + + return [ + new charts.Series( + id: 'Desktop', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: desktopSalesData, + ), + new charts.Series( + id: 'Tablet', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: tabletSalesData, + ), + new charts.Series( + id: 'Mobile', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: mobileSalesData, + ), + new charts.Series( + id: 'Other', + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: otherSalesData, + ), + ]; + } +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/animation_zoom.dart b/example/lib/line_chart/animation_zoom.dart new file mode 100644 index 0000000..5e5a327 --- /dev/null +++ b/example/lib/line_chart/animation_zoom.dart @@ -0,0 +1,101 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a line chart with pan and zoom enabled via +/// [Charts.PanAndZoomBehavior]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class LineAnimationZoomChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + LineAnimationZoomChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory LineAnimationZoomChart.withSampleData() { + return new LineAnimationZoomChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LineAnimationZoomChart.withRandomData() { + return new LineAnimationZoomChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = []; + + for (var i = 0; i < 100; i++) { + data.add(new LinearSales(i, random.nextInt(100))); + } + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + new charts.PanAndZoomBehavior(), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/area_and_line.dart b/example/lib/line_chart/area_and_line.dart new file mode 100644 index 0000000..cf69493 --- /dev/null +++ b/example/lib/line_chart/area_and_line.dart @@ -0,0 +1,141 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class AreaAndLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + AreaAndLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory AreaAndLineChart.withSampleData() { + return new AreaAndLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory AreaAndLineChart.withRandomData() { + return new AreaAndLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customArea'), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + animate: animate, + customSeriesRenderers: [ + new charts.LineRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customArea', + includeArea: true, + stacked: true), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + var myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ) + // Configure our custom bar target renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customArea'), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/dash_pattern.dart b/example/lib/line_chart/dash_pattern.dart new file mode 100644 index 0000000..f5da3be --- /dev/null +++ b/example/lib/line_chart/dash_pattern.dart @@ -0,0 +1,162 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Dash pattern line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +/// Example of a line chart rendered with dash patterns. +class DashPatternLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DashPatternLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory DashPatternLineChart.withSampleData() { + return new DashPatternLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DashPatternLineChart.withRandomData() { + return new DashPatternLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + dashPatternFn: (_, __) => [2, 2], + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate); + } + + /// Create three series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + var myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + var myFakeMobileData = [ + new LinearSales(0, 15), + new LinearSales(1, 75), + new LinearSales(2, 300), + new LinearSales(3, 225), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + dashPatternFn: (_, __) => [2, 2], + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + dashPatternFn: (_, __) => [8, 3, 2, 3], + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/line_annotation.dart b/example/lib/line_chart/line_annotation.dart new file mode 100644 index 0000000..2fd1f48 --- /dev/null +++ b/example/lib/line_chart/line_annotation.dart @@ -0,0 +1,124 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Line chart with line annotations example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class LineLineAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + LineLineAnnotationChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and line annotations. + /// + /// The second annotation extends beyond the range of the series data, + /// demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] flag. + /// This can be set to false to disable range extension. + factory LineLineAnnotationChart.withSampleData() { + return new LineLineAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LineLineAnnotationChart.withRandomData() { + return new LineLineAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + // Fix one of the points to 100 so that the annotations are consistently + // placed. + new LinearSales(3, 100), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + new charts.RangeAnnotation([ + new charts.LineAnnotationSegment( + 1.0, charts.RangeAnnotationAxisType.domain, + startLabel: 'Domain 1'), + new charts.LineAnnotationSegment( + 4, charts.RangeAnnotationAxisType.domain, + endLabel: 'Domain 2', color: charts.MaterialPalette.gray.shade200), + new charts.LineAnnotationSegment( + 20, charts.RangeAnnotationAxisType.measure, + startLabel: 'Measure 1 Start', + endLabel: 'Measure 1 End', + color: charts.MaterialPalette.gray.shade300), + new charts.LineAnnotationSegment( + 65, charts.RangeAnnotationAxisType.measure, + startLabel: 'Measure 2 Start', + endLabel: 'Measure 2 End', + color: charts.MaterialPalette.gray.shade400), + ]), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/line_gallery.dart b/example/lib/line_chart/line_gallery.dart new file mode 100644 index 0000000..edd29a4 --- /dev/null +++ b/example/lib/line_chart/line_gallery.dart @@ -0,0 +1,113 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'animation_zoom.dart'; +import 'area_and_line.dart'; +import 'dash_pattern.dart'; +import 'line_annotation.dart'; +import 'points.dart'; +import 'range_annotation.dart'; +import 'range_annotation_margin.dart'; +import 'segments.dart'; +import 'simple.dart'; +import 'simple_nulls.dart'; +import 'stacked_area.dart'; +import 'stacked_area_custom_color.dart'; +import 'stacked_area_nulls.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Simple Line Chart', + subtitle: 'With a single series and default line point highlighter', + childBuilder: () => new SimpleLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Stacked Area Chart', + subtitle: 'Stacked area chart with three series', + childBuilder: () => new StackedAreaLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Stacked Area Custom Color Chart', + subtitle: 'Stacked area chart with custom area skirt color', + childBuilder: () => new StackedAreaCustomColorLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Area and Line Combo Chart', + subtitle: 'Combo chart with one line series and one area series', + childBuilder: () => new AreaAndLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Points Line Chart', + subtitle: 'Line chart with points on a single series', + childBuilder: () => new PointsLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Null Data Line Chart', + subtitle: 'With a single series and null measure values', + childBuilder: () => new SimpleNullsLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Stacked Area with Nulls Chart', + subtitle: 'Stacked area chart with three series and null measure values', + childBuilder: () => new StackedAreaNullsLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Dash Pattern Line Chart', + subtitle: 'Line chart with dash patterns', + childBuilder: () => new DashPatternLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Segments Line Chart', + subtitle: 'Line chart with changes of style for each line', + childBuilder: () => new SegmentsLineChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Line Annotation Line Chart', + subtitle: 'Line chart with line annotations', + childBuilder: () => new LineLineAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Range Annotation Line Chart', + subtitle: 'Line chart with range annotations', + childBuilder: () => new LineRangeAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Range Annotation Margin Labels Line Chart', + subtitle: 'Line chart with range annotations with labels in margins', + childBuilder: () => new LineRangeAnnotationMarginChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Pan and Zoom Line Chart', + subtitle: 'Simple line chart pan and zoom behaviors enabled', + childBuilder: () => new LineAnimationZoomChart.withRandomData(), + ), + ]; +} diff --git a/example/lib/line_chart/points.dart b/example/lib/line_chart/points.dart new file mode 100644 index 0000000..02291dd --- /dev/null +++ b/example/lib/line_chart/points.dart @@ -0,0 +1,103 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class PointsLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PointsLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory PointsLineChart.withSampleData() { + return new PointsLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PointsLineChart.withRandomData() { + return new PointsLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + animate: animate, + defaultRenderer: new charts.LineRendererConfig(includePoints: true)); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/range_annotation.dart b/example/lib/line_chart/range_annotation.dart new file mode 100644 index 0000000..e9a8702 --- /dev/null +++ b/example/lib/line_chart/range_annotation.dart @@ -0,0 +1,124 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Line chart with range annotations example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class LineRangeAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + LineRangeAnnotationChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and range annotations. + /// + /// The second annotation extends beyond the range of the series data, + /// demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] flag. + /// This can be set to false to disable range extension. + factory LineRangeAnnotationChart.withSampleData() { + return new LineRangeAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LineRangeAnnotationChart.withRandomData() { + return new LineRangeAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + // Fix one of the points to 100 so that the annotations are consistently + // placed. + new LinearSales(3, 100), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate, behaviors: [ + new charts.RangeAnnotation([ + new charts.RangeAnnotationSegment( + 0.5, 1.0, charts.RangeAnnotationAxisType.domain, + startLabel: 'Domain 1'), + new charts.RangeAnnotationSegment( + 2, 4, charts.RangeAnnotationAxisType.domain, + endLabel: 'Domain 2', color: charts.MaterialPalette.gray.shade200), + new charts.RangeAnnotationSegment( + 15, 20, charts.RangeAnnotationAxisType.measure, + startLabel: 'Measure 1 Start', + endLabel: 'Measure 1 End', + color: charts.MaterialPalette.gray.shade300), + new charts.RangeAnnotationSegment( + 35, 65, charts.RangeAnnotationAxisType.measure, + startLabel: 'Measure 2 Start', + endLabel: 'Measure 2 End', + color: charts.MaterialPalette.gray.shade400), + ]), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/range_annotation_margin.dart b/example/lib/line_chart/range_annotation_margin.dart new file mode 100644 index 0000000..eaa5d75 --- /dev/null +++ b/example/lib/line_chart/range_annotation_margin.dart @@ -0,0 +1,141 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a line chart with range annotations configured to render labels +/// in the chart margin area. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class LineRangeAnnotationMarginChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + LineRangeAnnotationMarginChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and range annotations. + /// + /// The second annotation extends beyond the range of the series data, + /// demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] flag. + /// This can be set to false to disable range extension. + factory LineRangeAnnotationMarginChart.withSampleData() { + return new LineRangeAnnotationMarginChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory LineRangeAnnotationMarginChart.withRandomData() { + return new LineRangeAnnotationMarginChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + // Fix one of the points to 100 so that the annotations are consistently + // placed. + new LinearSales(3, 100), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + animate: animate, + + // Allow enough space in the left and right chart margins for the + // annotations. + layoutConfig: new charts.LayoutConfig( + leftMarginSpec: new charts.MarginSpec.fixedPixel(60), + topMarginSpec: new charts.MarginSpec.fixedPixel(20), + rightMarginSpec: new charts.MarginSpec.fixedPixel(60), + bottomMarginSpec: new charts.MarginSpec.fixedPixel(20)), + behaviors: [ + // Define one domain and two measure annotations configured to render + // labels in the chart margins. + new charts.RangeAnnotation([ + new charts.RangeAnnotationSegment( + 0.5, 1.0, charts.RangeAnnotationAxisType.domain, + startLabel: 'D1 Start', + endLabel: 'D1 End', + labelAnchor: charts.AnnotationLabelAnchor.end, + color: charts.MaterialPalette.gray.shade200, + // Override the default vertical direction for domain labels. + labelDirection: charts.AnnotationLabelDirection.horizontal), + new charts.RangeAnnotationSegment( + 15, 20, charts.RangeAnnotationAxisType.measure, + startLabel: 'M1 Start', + endLabel: 'M1 End', + labelAnchor: charts.AnnotationLabelAnchor.end, + color: charts.MaterialPalette.gray.shade300), + new charts.RangeAnnotationSegment( + 35, 65, charts.RangeAnnotationAxisType.measure, + startLabel: 'M2 Start', + endLabel: 'M2 End', + labelAnchor: charts.AnnotationLabelAnchor.start, + color: charts.MaterialPalette.gray.shade400), + ], defaultLabelPosition: charts.AnnotationLabelPosition.margin), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/segments.dart b/example/lib/line_chart/segments.dart new file mode 100644 index 0000000..245778f --- /dev/null +++ b/example/lib/line_chart/segments.dart @@ -0,0 +1,233 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a stacked area chart with changing styles within each line. +/// +/// Each series of data in this example contains different values for color, +/// dashPattern, or strokeWidthPx between each datum. The line and area skirt +/// will be rendered in segments, with the styling of the series changing when +/// these data attributes change. +/// +/// Note that if a dashPattern or strokeWidth value is not found for a +/// particular datum, then the chart will fall back to use the value defined in +/// the [charts.LineRendererConfig]. This could be used, for example, to define +/// a default dash pattern for the series, with only a specific datum called out +/// with a different pattern. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SegmentsLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SegmentsLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory SegmentsLineChart.withSampleData() { + return new SegmentsLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SegmentsLineChart.withRandomData() { + return new SegmentsLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + // Series of data with static dash pattern and stroke width. The colorFn + // accessor will colorize each datum (for all three series). + final colorChangeData = [ + new LinearSales(0, random.nextInt(100), null, 2.0), + new LinearSales(1, random.nextInt(100), null, 2.0), + new LinearSales(2, random.nextInt(100), null, 2.0), + new LinearSales(3, random.nextInt(100), null, 2.0), + new LinearSales(4, random.nextInt(100), null, 2.0), + new LinearSales(5, random.nextInt(100), null, 2.0), + new LinearSales(6, random.nextInt(100), null, 2.0), + ]; + + // Series of data with changing color and dash pattern. + final dashPatternChangeData = [ + new LinearSales(0, random.nextInt(100), [2, 2], 2.0), + new LinearSales(1, random.nextInt(100), [2, 2], 2.0), + new LinearSales(2, random.nextInt(100), [4, 4], 2.0), + new LinearSales(3, random.nextInt(100), [4, 4], 2.0), + new LinearSales(4, random.nextInt(100), [4, 4], 2.0), + new LinearSales(5, random.nextInt(100), [8, 3, 2, 3], 2.0), + new LinearSales(6, random.nextInt(100), [8, 3, 2, 3], 2.0), + ]; + + // Series of data with changing color and stroke width. + final strokeWidthChangeData = [ + new LinearSales(0, random.nextInt(100), null, 2.0), + new LinearSales(1, random.nextInt(100), null, 2.0), + new LinearSales(2, random.nextInt(100), null, 4.0), + new LinearSales(3, random.nextInt(100), null, 4.0), + new LinearSales(4, random.nextInt(100), null, 4.0), + new LinearSales(5, random.nextInt(100), null, 6.0), + new LinearSales(6, random.nextInt(100), null, 6.0), + ]; + + // Generate 2 shades of each color so that we can style the line segments. + final blue = charts.MaterialPalette.blue.makeShades(2); + final red = charts.MaterialPalette.red.makeShades(2); + final green = charts.MaterialPalette.green.makeShades(2); + + return [ + new charts.Series( + id: 'Color Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? blue[1] : blue[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: colorChangeData, + ), + new charts.Series( + id: 'Dash Pattern Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? red[1] : red[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: dashPatternChangeData, + ), + new charts.Series( + id: 'Stroke Width Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? green[1] : green[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: strokeWidthChangeData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + // Series of data with static dash pattern and stroke width. The colorFn + // accessor will colorize each datum (for all three series). + final colorChangeData = [ + new LinearSales(0, 5, null, 2.0), + new LinearSales(1, 15, null, 2.0), + new LinearSales(2, 25, null, 2.0), + new LinearSales(3, 75, null, 2.0), + new LinearSales(4, 100, null, 2.0), + new LinearSales(5, 90, null, 2.0), + new LinearSales(6, 75, null, 2.0), + ]; + + // Series of data with changing color and dash pattern. + final dashPatternChangeData = [ + new LinearSales(0, 5, [2, 2], 2.0), + new LinearSales(1, 15, [2, 2], 2.0), + new LinearSales(2, 25, [4, 4], 2.0), + new LinearSales(3, 75, [4, 4], 2.0), + new LinearSales(4, 100, [4, 4], 2.0), + new LinearSales(5, 90, [8, 3, 2, 3], 2.0), + new LinearSales(6, 75, [8, 3, 2, 3], 2.0), + ]; + + // Series of data with changing color and stroke width. + final strokeWidthChangeData = [ + new LinearSales(0, 5, null, 2.0), + new LinearSales(1, 15, null, 2.0), + new LinearSales(2, 25, null, 4.0), + new LinearSales(3, 75, null, 4.0), + new LinearSales(4, 100, null, 4.0), + new LinearSales(5, 90, null, 6.0), + new LinearSales(6, 75, null, 6.0), + ]; + + // Generate 2 shades of each color so that we can style the line segments. + final blue = charts.MaterialPalette.blue.makeShades(2); + final red = charts.MaterialPalette.red.makeShades(2); + final green = charts.MaterialPalette.green.makeShades(2); + + return [ + new charts.Series( + id: 'Color Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? blue[1] : blue[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: colorChangeData, + ), + new charts.Series( + id: 'Dash Pattern Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? red[1] : red[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: dashPatternChangeData, + ), + new charts.Series( + id: 'Stroke Width Change', + // Light shade for even years, dark shade for odd. + colorFn: (LinearSales sales, _) => + sales.year % 2 == 0 ? green[1] : green[0], + dashPatternFn: (LinearSales sales, _) => sales.dashPattern, + strokeWidthPxFn: (LinearSales sales, _) => sales.strokeWidthPx, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: strokeWidthChangeData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final List? dashPattern; + final double strokeWidthPx; + + LinearSales(this.year, this.sales, this.dashPattern, this.strokeWidthPx); +} diff --git a/example/lib/line_chart/simple.dart b/example/lib/line_chart/simple.dart new file mode 100644 index 0000000..f8b79cc --- /dev/null +++ b/example/lib/line_chart/simple.dart @@ -0,0 +1,101 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a simple line chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SimpleLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory SimpleLineChart.withSampleData() { + return new SimpleLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleLineChart.withRandomData() { + return new SimpleLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/simple_nulls.dart b/example/lib/line_chart/simple_nulls.dart new file mode 100644 index 0000000..2873477 --- /dev/null +++ b/example/lib/line_chart/simple_nulls.dart @@ -0,0 +1,179 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a line chart with null measure values. +/// +/// Null values will be visible as gaps in lines and area skirts. Any data +/// points that exist between two nulls in a line will be rendered as an +/// isolated point, as seen in the green series. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SimpleNullsLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleNullsLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory SimpleNullsLineChart.withSampleData() { + return new SimpleNullsLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleNullsLineChart.withRandomData() { + return new SimpleNullsLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, null), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, null), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, null), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 15), + new LinearSales(2, null), + new LinearSales(3, 75), + new LinearSales(4, 100), + new LinearSales(5, 90), + new LinearSales(6, 75), + ]; + + final myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 30), + new LinearSales(2, 50), + new LinearSales(3, 150), + new LinearSales(4, 200), + new LinearSales(5, 180), + new LinearSales(6, 150), + ]; + + final myFakeMobileData = [ + new LinearSales(0, 15), + new LinearSales(1, 45), + new LinearSales(2, null), + new LinearSales(3, 225), + new LinearSales(4, null), + new LinearSales(5, 270), + new LinearSales(6, 225), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int? sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/stacked_area.dart b/example/lib/line_chart/stacked_area.dart new file mode 100644 index 0000000..5a7ae58 --- /dev/null +++ b/example/lib/line_chart/stacked_area.dart @@ -0,0 +1,160 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a stacked area chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class StackedAreaLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedAreaLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory StackedAreaLineChart.withSampleData() { + return new StackedAreaLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedAreaLineChart.withRandomData() { + return new StackedAreaLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + var myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + var myFakeMobileData = [ + new LinearSales(0, 15), + new LinearSales(1, 75), + new LinearSales(2, 300), + new LinearSales(3, 225), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/stacked_area_custom_color.dart b/example/lib/line_chart/stacked_area_custom_color.dart new file mode 100644 index 0000000..ce6401a --- /dev/null +++ b/example/lib/line_chart/stacked_area_custom_color.dart @@ -0,0 +1,175 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a stacked area chart with custom area colors. +/// +/// By default, the area skirt for a chart will be drawn with the same color as +/// the line, but with a 10% opacity assigned to it. An area color function can +/// be provided to override this with any custom color. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class StackedAreaCustomColorLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedAreaCustomColorLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory StackedAreaCustomColorLineChart.withSampleData() { + return new StackedAreaCustomColorLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedAreaCustomColorLineChart.withRandomData() { + return new StackedAreaCustomColorLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 25), + new LinearSales(2, 100), + new LinearSales(3, 75), + ]; + + var myFakeTabletData = [ + new LinearSales(0, 10), + new LinearSales(1, 50), + new LinearSales(2, 200), + new LinearSales(3, 150), + ]; + + var myFakeMobileData = [ + new LinearSales(0, 15), + new LinearSales(1, 75), + new LinearSales(2, 300), + new LinearSales(3, 225), + ]; + + return [ + new charts.Series( + id: 'Desktop', + // colorFn specifies that the line will be blue. + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + // areaColorFn specifies that the area skirt will be light blue. + areaColorFn: (_, __) => + charts.MaterialPalette.blue.shadeDefault.lighter, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + // colorFn specifies that the line will be red. + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + // areaColorFn specifies that the area skirt will be light red. + areaColorFn: (_, __) => charts.MaterialPalette.red.shadeDefault.lighter, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + // colorFn specifies that the line will be green. + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + // areaColorFn specifies that the area skirt will be light green. + areaColorFn: (_, __) => + charts.MaterialPalette.green.shadeDefault.lighter, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/line_chart/stacked_area_nulls.dart b/example/lib/line_chart/stacked_area_nulls.dart new file mode 100644 index 0000000..89e24a1 --- /dev/null +++ b/example/lib/line_chart/stacked_area_nulls.dart @@ -0,0 +1,191 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a stacked area chart with null measure values. +/// +/// Null values will be visible as gaps in lines and area skirts. Any data +/// points that exist between two nulls in a line will be rendered as an +/// isolated point, as seen in the green series. +/// +/// In a stacked area chart, no data above a null value in the stack will be +/// rendered. In this example, the null measure value at domain 2 in the Desktop +/// series will prevent any data from being rendered at domain 2 for every +/// series because it is at the bottom of the stack. +/// +/// This will also result in an isolated point being rendered for the domain +/// value 3 in the Mobile series, because that series also contains a null at +/// domain 4. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class StackedAreaNullsLineChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + StackedAreaNullsLineChart(this.seriesList, {this.animate = false}); + + /// Creates a [LineChart] with sample data and no transition. + factory StackedAreaNullsLineChart.withSampleData() { + return new StackedAreaNullsLineChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory StackedAreaNullsLineChart.withRandomData() { + return new StackedAreaNullsLineChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myFakeDesktopData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, null), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + var myFakeTabletData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, random.nextInt(100)), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + var myFakeMobileData = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + new LinearSales(4, null), + new LinearSales(5, random.nextInt(100)), + new LinearSales(6, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.LineChart(seriesList, + defaultRenderer: + new charts.LineRendererConfig(includeArea: true, stacked: true), + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(0, 5), + new LinearSales(1, 15), + new LinearSales(2, null), + new LinearSales(3, 75), + new LinearSales(4, 100), + new LinearSales(5, 90), + new LinearSales(6, 75), + ]; + + final myFakeTabletData = [ + new LinearSales(0, 5), + new LinearSales(1, 15), + new LinearSales(2, 25), + new LinearSales(3, 75), + new LinearSales(4, 100), + new LinearSales(5, 90), + new LinearSales(6, 75), + ]; + + final myFakeMobileData = [ + new LinearSales(0, 5), + new LinearSales(1, 15), + new LinearSales(2, 25), + new LinearSales(3, 75), + new LinearSales(4, null), + new LinearSales(5, 90), + new LinearSales(6, 75), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeTabletData, + ), + new charts.Series( + id: 'Mobile', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: myFakeMobileData, + ), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int? sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..13fd48f --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,54 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'app_config.dart'; +import 'home.dart'; + +/// The main gallery app widget. +class GalleryApp extends StatefulWidget { + GalleryApp({Key? key}) : super(key: key); + + @override + GalleryAppState createState() => new GalleryAppState(); +} + +/// The main gallery app state. +/// +/// Controls performance overlay, and instantiates a [Home] widget. +class GalleryAppState extends State { + // Initialize app settings from the default configuration. + bool _showPerformanceOverlay = defaultConfig.showPerformanceOverlay; + + @override + Widget build(BuildContext context) { + return new MaterialApp( + title: defaultConfig.appName, + theme: defaultConfig.theme, + showPerformanceOverlay: _showPerformanceOverlay, + home: new Home( + showPerformanceOverlay: _showPerformanceOverlay, + onShowPerformanceOverlayChanged: (bool value) { + setState(() { + _showPerformanceOverlay = value; + }); + }, + )); + } +} + +void main() { + runApp(new GalleryApp()); +} diff --git a/example/lib/pie_chart/auto_label.dart b/example/lib/pie_chart/auto_label.dart new file mode 100644 index 0000000..60cafdf --- /dev/null +++ b/example/lib/pie_chart/auto_label.dart @@ -0,0 +1,123 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Donut chart with labels example. This is a simple pie chart with a hole in +/// the middle. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class DonutAutoLabelChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DonutAutoLabelChart(this.seriesList, {this.animate = false}); + + /// Creates a [PieChart] with sample data and no transition. + factory DonutAutoLabelChart.withSampleData() { + return new DonutAutoLabelChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DonutAutoLabelChart.withRandomData() { + return new DonutAutoLabelChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.year}: ${row.sales}', + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Configure the width of the pie slices to 60px. The remaining space in + // the chart will be left as a hole in the center. + // + // [ArcLabelDecorator] will automatically position the label inside the + // arc if the label will fit. If the label will not fit, it will draw + // outside of the arc with a leader line. Labels can always display + // inside or outside using [LabelPosition]. + // + // Text style for inside / outside can be controlled independently by + // setting [insideLabelStyleSpec] and [outsideLabelStyleSpec]. + // + // Example configuring different styles for inside/outside: + // new charts.ArcLabelDecorator( + // insideLabelStyleSpec: new charts.TextStyleSpec(...), + // outsideLabelStyleSpec: new charts.TextStyleSpec(...)), + defaultRenderer: new charts.ArcRendererConfig( + arcWidth: 60, + arcRendererDecorators: [new charts.ArcLabelDecorator()])); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.year}: ${row.sales}', + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/pie_chart/donut.dart b/example/lib/pie_chart/donut.dart new file mode 100644 index 0000000..40c9051 --- /dev/null +++ b/example/lib/pie_chart/donut.dart @@ -0,0 +1,103 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Donut chart example. This is a simple pie chart with a hole in the middle. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class DonutPieChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + DonutPieChart(this.seriesList, {this.animate = false}); + + /// Creates a [PieChart] with sample data and no transition. + factory DonutPieChart.withSampleData() { + return new DonutPieChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory DonutPieChart.withRandomData() { + return new DonutPieChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Configure the width of the pie slices to 60px. The remaining space in + // the chart will be left as a hole in the center. + defaultRenderer: new charts.ArcRendererConfig(arcWidth: 60)); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/pie_chart/gauge.dart b/example/lib/pie_chart/gauge.dart new file mode 100644 index 0000000..5ed4873 --- /dev/null +++ b/example/lib/pie_chart/gauge.dart @@ -0,0 +1,106 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Gauge chart example, where the data does not cover a full revolution in the +/// chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class GaugeChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + GaugeChart(this.seriesList, {this.animate = false}); + + /// Creates a [PieChart] with sample data and no transition. + factory GaugeChart.withSampleData() { + return new GaugeChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory GaugeChart.withRandomData() { + return new GaugeChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new GaugeSegment('Low', random.nextInt(100)), + new GaugeSegment('Acceptable', random.nextInt(100)), + new GaugeSegment('High', random.nextInt(100)), + new GaugeSegment('Highly Unusual', random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Segments', + domainFn: (GaugeSegment segment, _) => segment.segment, + measureFn: (GaugeSegment segment, _) => segment.size, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Configure the width of the pie slices to 30px. The remaining space in + // the chart will be left as a hole in the center. Adjust the start + // angle and the arc length of the pie so it resembles a gauge. + defaultRenderer: new charts.ArcRendererConfig( + arcWidth: 30, startAngle: 4 / 5 * pi, arcLength: 7 / 5 * pi)); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new GaugeSegment('Low', 75), + new GaugeSegment('Acceptable', 100), + new GaugeSegment('High', 50), + new GaugeSegment('Highly Unusual', 5), + ]; + + return [ + new charts.Series( + id: 'Segments', + domainFn: (GaugeSegment segment, _) => segment.segment, + measureFn: (GaugeSegment segment, _) => segment.size, + data: data, + ) + ]; + } +} + +/// Sample data type. +class GaugeSegment { + final String segment; + final int size; + + GaugeSegment(this.segment, this.size); +} diff --git a/example/lib/pie_chart/outside_label.dart b/example/lib/pie_chart/outside_label.dart new file mode 100644 index 0000000..1db0ad4 --- /dev/null +++ b/example/lib/pie_chart/outside_label.dart @@ -0,0 +1,118 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Simple pie chart with outside labels example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class PieOutsideLabelChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PieOutsideLabelChart(this.seriesList, {this.animate = false}); + + /// Creates a [PieChart] with sample data and no transition. + factory PieOutsideLabelChart.withSampleData() { + return new PieOutsideLabelChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PieOutsideLabelChart.withRandomData() { + return new PieOutsideLabelChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.year}: ${row.sales}', + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, + animate: animate, + // Add an [ArcLabelDecorator] configured to render labels outside of the + // arc with a leader line. + // + // Text style for inside / outside can be controlled independently by + // setting [insideLabelStyleSpec] and [outsideLabelStyleSpec]. + // + // Example configuring different styles for inside/outside: + // new charts.ArcLabelDecorator( + // insideLabelStyleSpec: new charts.TextStyleSpec(...), + // outsideLabelStyleSpec: new charts.TextStyleSpec(...)), + defaultRenderer: new charts.ArcRendererConfig(arcRendererDecorators: [ + new charts.ArcLabelDecorator( + labelPosition: charts.ArcLabelPosition.outside) + ])); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + // Set a label accessor to control the text of the arc label. + labelAccessorFn: (LinearSales row, _) => '${row.year}: ${row.sales}', + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/pie_chart/partial_pie.dart b/example/lib/pie_chart/partial_pie.dart new file mode 100644 index 0000000..1b32f71 --- /dev/null +++ b/example/lib/pie_chart/partial_pie.dart @@ -0,0 +1,104 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Partial pie chart example, where the data does not cover a full revolution +/// in the chart. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class PartialPieChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + PartialPieChart(this.seriesList, {this.animate = false}); + + /// Creates a [PieChart] with sample data and no transition. + factory PartialPieChart.withSampleData() { + return new PartialPieChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory PartialPieChart.withRandomData() { + return new PartialPieChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + // Configure the pie to display the data across only 3/4 instead of the full + // revolution. + return new charts.PieChart(seriesList, + animate: animate, + defaultRenderer: new charts.ArcRendererConfig(arcLength: 3 / 2 * pi)); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/pie_chart/pie_gallery.dart b/example/lib/pie_chart/pie_gallery.dart new file mode 100644 index 0000000..56c9178 --- /dev/null +++ b/example/lib/pie_chart/pie_gallery.dart @@ -0,0 +1,65 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'auto_label.dart'; +import 'donut.dart'; +import 'gauge.dart'; +import 'simple.dart'; +import 'outside_label.dart'; +import 'partial_pie.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Simple Pie Chart', + subtitle: 'With a single series', + childBuilder: () => new SimplePieChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Outside Label Pie Chart', + subtitle: 'With a single series and labels outside the arcs', + childBuilder: () => new PieOutsideLabelChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Partial Pie Chart', + subtitle: 'That doesn\'t cover a full revolution', + childBuilder: () => new PartialPieChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Simple Donut Chart', + subtitle: 'With a single series and a hole in the middle', + childBuilder: () => new DonutPieChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Auto Label Donut Chart', + subtitle: + 'With a single series, a hole in the middle, and auto-positioned labels', + childBuilder: () => new DonutAutoLabelChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.pie_chart), + title: 'Gauge Chart', + subtitle: 'That doesn\'t cover a full revolution', + childBuilder: () => new GaugeChart.withRandomData(), + ), + ]; +} diff --git a/example/lib/pie_chart/simple.dart b/example/lib/pie_chart/simple.dart new file mode 100644 index 0000000..51b30b1 --- /dev/null +++ b/example/lib/pie_chart/simple.dart @@ -0,0 +1,99 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Simple pie chart example. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SimplePieChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimplePieChart(this.seriesList, {this.animate = false}); + + /// Creates a [PieChart] with sample data and no transition. + factory SimplePieChart.withSampleData() { + return new SimplePieChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimplePieChart.withRandomData() { + return new SimplePieChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new LinearSales(0, random.nextInt(100)), + new LinearSales(1, random.nextInt(100)), + new LinearSales(2, random.nextInt(100)), + new LinearSales(3, random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.PieChart(seriesList, animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 100), + new LinearSales(1, 75), + new LinearSales(2, 25), + new LinearSales(3, 5), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + + LinearSales(this.year, this.sales); +} diff --git a/example/lib/scatter_plot_chart/animation_zoom.dart b/example/lib/scatter_plot_chart/animation_zoom.dart new file mode 100644 index 0000000..4adea61 --- /dev/null +++ b/example/lib/scatter_plot_chart/animation_zoom.dart @@ -0,0 +1,144 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a line chart with pan and zoom enabled via +/// [Charts.PanAndZoomBehavior]. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class ScatterPlotAnimationZoomChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + ScatterPlotAnimationZoomChart(this.seriesList, {this.animate = false}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory ScatterPlotAnimationZoomChart.withSampleData() { + return new ScatterPlotAnimationZoomChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ScatterPlotAnimationZoomChart.withRandomData() { + return new ScatterPlotAnimationZoomChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = []; + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + for (var i = 0; i < 100; i++) { + data.add(new LinearSales(i, random.nextInt(100), makeRadius(4))); + } + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + behaviors: [ + new charts.PanAndZoomBehavior(), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5, 3.0), + new LinearSales(10, 25, 5.0), + new LinearSales(12, 75, 4.0), + new LinearSales(13, 225, 5.0), + new LinearSales(16, 50, 4.0), + new LinearSales(24, 75, 3.0), + new LinearSales(25, 100, 3.0), + new LinearSales(34, 150, 5.0), + new LinearSales(37, 10, 4.5), + new LinearSales(45, 300, 8.0), + new LinearSales(52, 15, 4.0), + new LinearSales(56, 200, 7.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + + LinearSales(this.year, this.sales, this.radius); +} diff --git a/example/lib/scatter_plot_chart/bucketing_axis.dart b/example/lib/scatter_plot_chart/bucketing_axis.dart new file mode 100644 index 0000000..0ea045d --- /dev/null +++ b/example/lib/scatter_plot_chart/bucketing_axis.dart @@ -0,0 +1,264 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a scatter plot chart with a bucketing measure axis and a legend. +/// +/// A bucketing measure axis 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. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class BucketingAxisScatterPlotChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + BucketingAxisScatterPlotChart(this.seriesList, {this.animate = false}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory BucketingAxisScatterPlotChart.withSampleData() { + return new BucketingAxisScatterPlotChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory BucketingAxisScatterPlotChart.withRandomData() { + return new BucketingAxisScatterPlotChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 6).toDouble(); + + // Make sure that the measure values for the first five series are well + // above the threshold. This simulates the grouping of the small values into + // the "Other" series. + final myFakeDesktopData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + final myFakeTabletData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + final myFakeMobileData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + final myFakeChromebookData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + final myFakeHomeData = [ + new LinearSales( + random.nextInt(100), (random.nextInt(50) + 50) / 100, makeRadius(6)), + ]; + + // Make sure that the "Other" series values are smaller. + final myFakeOtherData = [ + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + new LinearSales( + random.nextInt(100), random.nextInt(50) / 100, makeRadius(6)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeDesktopData), + new charts.Series( + id: 'Tablet', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeTabletData), + new charts.Series( + id: 'Mobile', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeMobileData), + new charts.Series( + id: 'Chromebook', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeChromebookData), + new charts.Series( + id: 'Home', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.indigo.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeHomeData), + new charts.Series( + id: 'Other', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.gray.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeOtherData), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + // Set up a bucketing axis that will place all values below 0.1 (10%) + // into a bucket at the bottom of the chart. + // + // Configure a tick count of 3 so that we get 100%, 50%, and the + // threshold. + primaryMeasureAxis: new charts.BucketingAxisSpec( + threshold: 0.1, + tickProviderSpec: new charts.BucketingNumericTickProviderSpec( + desiredTickCount: 3)), + // Add a series legend to display the series names. + behaviors: [ + new charts.SeriesLegend(position: charts.BehaviorPosition.end), + ], + animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myFakeDesktopData = [ + new LinearSales(52, 0.75, 14.0), + ]; + + final myFakeTabletData = [ + new LinearSales(45, 0.3, 18.0), + ]; + + final myFakeMobileData = [ + new LinearSales(56, 0.8, 17.0), + ]; + + final myFakeChromebookData = [ + new LinearSales(25, 0.6, 13.0), + ]; + + final myFakeHomeData = [ + new LinearSales(34, 0.5, 15.0), + ]; + + final myFakeOtherData = [ + new LinearSales(10, 0.25, 15.0), + new LinearSales(12, 0.075, 14.0), + new LinearSales(13, 0.225, 15.0), + new LinearSales(16, 0.03, 14.0), + new LinearSales(24, 0.04, 13.0), + new LinearSales(37, 0.1, 14.5), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.blue.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeDesktopData), + new charts.Series( + id: 'Tablet', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.red.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeTabletData), + new charts.Series( + id: 'Mobile', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.green.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeMobileData), + new charts.Series( + id: 'Chromebook', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.purple.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeChromebookData), + new charts.Series( + id: 'Home', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.indigo.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeHomeData), + new charts.Series( + id: 'Other', + colorFn: (LinearSales sales, _) => + charts.MaterialPalette.gray.shadeDefault, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.revenueShare, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: myFakeOtherData), + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final double revenueShare; + final double radius; + + LinearSales(this.year, this.revenueShare, this.radius); +} diff --git a/example/lib/scatter_plot_chart/comparison_points.dart b/example/lib/scatter_plot_chart/comparison_points.dart new file mode 100644 index 0000000..2412d54 --- /dev/null +++ b/example/lib/scatter_plot_chart/comparison_points.dart @@ -0,0 +1,169 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Line chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class ComparisonPointsScatterPlotChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + ComparisonPointsScatterPlotChart(this.seriesList, {this.animate = false}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory ComparisonPointsScatterPlotChart.withSampleData() { + return new ComparisonPointsScatterPlotChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ComparisonPointsScatterPlotChart.withRandomData() { + return new ComparisonPointsScatterPlotChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final maxMeasure = 100; + + final data = [ + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + _makeRandomDatum(maxMeasure, random), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + domainLowerBoundFn: (LinearSales sales, _) => sales.yearLower, + domainUpperBoundFn: (LinearSales sales, _) => sales.yearUpper, + measureFn: (LinearSales sales, _) => sales.sales, + measureLowerBoundFn: (LinearSales sales, _) => sales.salesLower, + measureUpperBoundFn: (LinearSales sales, _) => sales.salesUpper, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } + + static LinearSales _makeRandomDatum(int max, Random random) { + final makeRadius = (int value) => (random.nextInt(value) + 6).toDouble(); + + final year = random.nextInt(max); + final yearLower = (year * 0.8).round(); + final yearUpper = year; + final sales = random.nextInt(max); + final salesLower = (sales * 0.8).round(); + final salesUpper = sales; + + return new LinearSales(year, yearLower, yearUpper, sales, salesLower, + salesUpper, makeRadius(4)); + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + defaultRenderer: + new charts.PointRendererConfig(pointRendererDecorators: [ + new charts.ComparisonPointsDecorator( + symbolRenderer: new charts.CylinderSymbolRenderer()) + ])); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(10, 7, 10, 25, 20, 25, 5.0), + new LinearSales(13, 11, 13, 225, 205, 225, 5.0), + new LinearSales(34, 34, 24, 150, 150, 130, 5.0), + new LinearSales(37, 37, 57, 10, 10, 12, 6.5), + new LinearSales(45, 35, 45, 260, 300, 260, 8.0), + new LinearSales(56, 46, 56, 200, 170, 200, 7.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + domainLowerBoundFn: (LinearSales sales, _) => sales.yearLower, + domainUpperBoundFn: (LinearSales sales, _) => sales.yearUpper, + measureFn: (LinearSales sales, _) => sales.sales, + measureLowerBoundFn: (LinearSales sales, _) => sales.salesLower, + measureUpperBoundFn: (LinearSales sales, _) => sales.salesUpper, + // Providing a radius function is optional. + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int yearLower; + final int yearUpper; + final int sales; + final int salesLower; + final int salesUpper; + final double radius; + + LinearSales(this.year, this.yearLower, this.yearUpper, this.sales, + this.salesLower, this.salesUpper, this.radius); +} diff --git a/example/lib/scatter_plot_chart/scatter_plot_gallery.dart b/example/lib/scatter_plot_chart/scatter_plot_gallery.dart new file mode 100644 index 0000000..3c1b3f7 --- /dev/null +++ b/example/lib/scatter_plot_chart/scatter_plot_gallery.dart @@ -0,0 +1,58 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'animation_zoom.dart'; +import 'bucketing_axis.dart'; +import 'comparison_points.dart'; +import 'shapes.dart'; +import 'simple.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Simple Scatter Plot Chart', + subtitle: 'With a single series', + childBuilder: () => new SimpleScatterPlotChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Shapes Scatter Plot Chart', + subtitle: 'With custom shapes', + childBuilder: () => new ShapesScatterPlotChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Comparison Points Scatter Plot Chart', + subtitle: 'Scatter plot chart with comparison points', + childBuilder: () => new ComparisonPointsScatterPlotChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Pan and Zoom Scatter Plot Chart', + subtitle: 'Simple scatter plot chart pan and zoom behaviors enabled', + childBuilder: () => new ScatterPlotAnimationZoomChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.scatter_plot), + title: 'Bucketing Axis Scatter Plot Chart', + subtitle: 'Scatter plot with a measure axis that buckets values less ' + + 'than 10% into a single region below the draw area', + childBuilder: () => new BucketingAxisScatterPlotChart.withRandomData(), + ), + ]; +} diff --git a/example/lib/scatter_plot_chart/shapes.dart b/example/lib/scatter_plot_chart/shapes.dart new file mode 100644 index 0000000..1c0ed7f --- /dev/null +++ b/example/lib/scatter_plot_chart/shapes.dart @@ -0,0 +1,206 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a scatter plot chart using custom symbols for the points. +/// +/// The series has been configured to draw each point as a square by default. +/// +/// Some data will be drawn as a circle, indicated by defining a custom "circle" +/// value referenced by [pointSymbolRendererFnKey]. +/// +/// Some other data have will be drawn as a hollow circle. In addition to the +/// custom renderer key, these data also have stroke and fillColor values +/// defined. Configuring a separate fillColor will cause the center of the shape +/// to be filled in, with white in these examples. The border of the shape will +/// be color with the color of the data. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class ShapesScatterPlotChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + ShapesScatterPlotChart(this.seriesList, {this.animate = false}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory ShapesScatterPlotChart.withSampleData() { + return new ShapesScatterPlotChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory ShapesScatterPlotChart.withRandomData() { + return new ShapesScatterPlotChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + final data = [ + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + 'circle', null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + 'circle', null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6), + null, null, null), + // Render a hollow square, filled in with white. + new LinearSales(random.nextInt(100), random.nextInt(100), + makeRadius(4) + 4, null, charts.MaterialPalette.white, 2.0), + ]; + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + fillColorFn: (LinearSales row, _) => row.fillColor, + strokeWidthPxFn: (LinearSales row, _) => row.strokeWidth, + data: data, + ) + // Accessor function that associates each datum with a symbol renderer. + ..setAttribute( + charts.pointSymbolRendererFnKey, (int index) => data[index].shape) + // Default symbol renderer ID for data that have no defined shape. + ..setAttribute(charts.pointSymbolRendererIdKey, 'rect') + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, + animate: animate, + // Configure the point renderer to have a map of custom symbol + // renderers. + defaultRenderer: + new charts.PointRendererConfig(customSymbolRenderers: { + 'circle': new charts.CircleSymbolRenderer(), + 'rect': new charts.RectSymbolRenderer(), + })); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5, 3.0, 'circle', null, null), + new LinearSales(10, 25, 5.0, null, null, null), + new LinearSales(12, 75, 4.0, null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales( + 13, 225, 5.0, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(16, 50, 4.0, null, null, null), + new LinearSales(24, 75, 3.0, null, null, null), + new LinearSales(25, 100, 3.0, 'circle', null, null), + new LinearSales(34, 150, 5.0, null, null, null), + new LinearSales(37, 10, 4.5, null, null, null), + // Render a hollow circle, filled in with white. + new LinearSales( + 45, 300, 8.0, 'circle', charts.MaterialPalette.white, 2.0), + new LinearSales(52, 15, 4.0, null, null, null), + // Render a hollow square, filled in with white. + new LinearSales(56, 200, 7.0, null, charts.MaterialPalette.white, 2.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + fillColorFn: (LinearSales row, _) => row.fillColor, + strokeWidthPxFn: (LinearSales row, _) => row.strokeWidth, + data: data, + ) + // Accessor function that associates each datum with a symbol renderer. + ..setAttribute( + charts.pointSymbolRendererFnKey, (int index) => data[index].shape) + // Default symbol renderer ID for data that have no defined shape. + ..setAttribute(charts.pointSymbolRendererIdKey, 'rect') + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + final String? shape; + final charts.Color? fillColor; + final double? strokeWidth; + + LinearSales(this.year, this.sales, this.radius, this.shape, this.fillColor, + this.strokeWidth); +} diff --git a/example/lib/scatter_plot_chart/simple.dart b/example/lib/scatter_plot_chart/simple.dart new file mode 100644 index 0000000..6faf10b --- /dev/null +++ b/example/lib/scatter_plot_chart/simple.dart @@ -0,0 +1,150 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Scatter plot chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SimpleScatterPlotChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleScatterPlotChart(this.seriesList, {this.animate = false}); + + /// Creates a [ScatterPlotChart] with sample data and no transition. + factory SimpleScatterPlotChart.withSampleData() { + return new SimpleScatterPlotChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleScatterPlotChart.withRandomData() { + return new SimpleScatterPlotChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final makeRadius = (int value) => (random.nextInt(value) + 2).toDouble(); + + final data = [ + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + new LinearSales(random.nextInt(100), random.nextInt(100), makeRadius(6)), + ]; + + final maxMeasure = 100; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (LinearSales sales, _) { + // Color bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.ScatterPlotChart(seriesList, animate: animate); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new LinearSales(0, 5, 3.0), + new LinearSales(10, 25, 5.0), + new LinearSales(12, 75, 4.0), + new LinearSales(13, 225, 5.0), + new LinearSales(16, 50, 4.0), + new LinearSales(24, 75, 3.0), + new LinearSales(25, 100, 3.0), + new LinearSales(34, 150, 5.0), + new LinearSales(37, 10, 4.5), + new LinearSales(45, 300, 8.0), + new LinearSales(52, 15, 4.0), + new LinearSales(56, 200, 7.0), + ]; + + final maxMeasure = 300; + + return [ + new charts.Series( + id: 'Sales', + // Providing a color function is optional. + colorFn: (LinearSales sales, _) { + // Bucket the measure column value into 3 distinct colors. + final bucket = sales.sales / maxMeasure; + + if (bucket < 1 / 3) { + return charts.MaterialPalette.blue.shadeDefault; + } else if (bucket < 2 / 3) { + return charts.MaterialPalette.red.shadeDefault; + } else { + return charts.MaterialPalette.green.shadeDefault; + } + }, + domainFn: (LinearSales sales, _) => sales.year, + measureFn: (LinearSales sales, _) => sales.sales, + // Providing a radius function is optional. + radiusPxFn: (LinearSales sales, _) => sales.radius, + data: data, + ) + ]; + } +} + +/// Sample linear data type. +class LinearSales { + final int year; + final int sales; + final double radius; + + LinearSales(this.year, this.sales, this.radius); +} diff --git a/example/lib/time_series_chart/confidence_interval.dart b/example/lib/time_series_chart/confidence_interval.dart new file mode 100644 index 0000000..32b0722 --- /dev/null +++ b/example/lib/time_series_chart/confidence_interval.dart @@ -0,0 +1,119 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a time series chart with a confidence interval. +/// +/// Confidence interval is defined by specifying the upper and lower measure +/// bounds in the series. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class TimeSeriesConfidenceInterval extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesConfidenceInterval(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesConfidenceInterval.withSampleData() { + return new TimeSeriesConfidenceInterval( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesConfidenceInterval.withRandomData() { + return new TimeSeriesConfidenceInterval(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + // When the measureLowerBoundFn and measureUpperBoundFn is defined, + // the line renderer will render the area around the bounds. + measureLowerBoundFn: (TimeSeriesSales sales, _) => sales.sales - 5, + measureUpperBoundFn: (TimeSeriesSales sales, _) => sales.sales + 5, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + // When the measureLowerBoundFn and measureUpperBoundFn is defined, + // the line renderer will render the area around the bounds. + measureLowerBoundFn: (TimeSeriesSales sales, _) => sales.sales - 5, + measureUpperBoundFn: (TimeSeriesSales sales, _) => sales.sales + 5, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/example/lib/time_series_chart/end_points_axis.dart b/example/lib/time_series_chart/end_points_axis.dart new file mode 100644 index 0000000..50f9e76 --- /dev/null +++ b/example/lib/time_series_chart/end_points_axis.dart @@ -0,0 +1,111 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a time series chart with an end points domain axis. +/// +/// An end points axis generates two ticks, one at each end of the axis range. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class EndPointsAxisTimeSeriesChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + EndPointsAxisTimeSeriesChart(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory EndPointsAxisTimeSeriesChart.withSampleData() { + return new EndPointsAxisTimeSeriesChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory EndPointsAxisTimeSeriesChart.withRandomData() { + return new EndPointsAxisTimeSeriesChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Configures an axis spec that is configured to render one tick at each + // end of the axis range, anchored "inside" the axis. The start tick label + // will be left-aligned with its tick mark, and the end tick label will be + // right-aligned with its tick mark. + domainAxis: new charts.EndPointsTimeAxisSpec(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/example/lib/time_series_chart/line_annotation.dart b/example/lib/time_series_chart/line_annotation.dart new file mode 100644 index 0000000..9ab4a80 --- /dev/null +++ b/example/lib/time_series_chart/line_annotation.dart @@ -0,0 +1,115 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Time series chart with line annotation example +/// +/// The example future range annotation extends beyond the range of the series +/// data, demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] +/// flag. This can be set to false to disable range extension. +/// +/// Additional annotations may be added simply by adding additional +/// [Charts.RangeAnnotationSegment] items to the list. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class TimeSeriesLineAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesLineAnnotationChart(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesLineAnnotationChart.withSampleData() { + return new TimeSeriesLineAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesLineAnnotationChart.withRandomData() { + return new TimeSeriesLineAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, animate: animate, behaviors: [ + new charts.RangeAnnotation([ + new charts.LineAnnotationSegment( + new DateTime(2017, 10, 4), charts.RangeAnnotationAxisType.domain, + startLabel: 'Oct 4'), + new charts.LineAnnotationSegment( + new DateTime(2017, 10, 15), charts.RangeAnnotationAxisType.domain, + endLabel: 'Oct 15'), + ]), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/example/lib/time_series_chart/range_annotation.dart b/example/lib/time_series_chart/range_annotation.dart new file mode 100644 index 0000000..6501ff7 --- /dev/null +++ b/example/lib/time_series_chart/range_annotation.dart @@ -0,0 +1,111 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Time series chart with range annotation example +/// +/// The example future range annotation extends beyond the range of the series +/// data, demonstrating the effect of the [Charts.RangeAnnotation.extendAxis] +/// flag. This can be set to false to disable range extension. +/// +/// Additional annotations may be added simply by adding additional +/// [Charts.RangeAnnotationSegment] items to the list. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class TimeSeriesRangeAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesRangeAnnotationChart(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesRangeAnnotationChart.withSampleData() { + return new TimeSeriesRangeAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesRangeAnnotationChart.withRandomData() { + return new TimeSeriesRangeAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, animate: animate, behaviors: [ + new charts.RangeAnnotation([ + new charts.RangeAnnotationSegment(new DateTime(2017, 10, 4), + new DateTime(2017, 10, 15), charts.RangeAnnotationAxisType.domain), + ]), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/example/lib/time_series_chart/range_annotation_margin.dart b/example/lib/time_series_chart/range_annotation_margin.dart new file mode 100644 index 0000000..c05d5a1 --- /dev/null +++ b/example/lib/time_series_chart/range_annotation_margin.dart @@ -0,0 +1,139 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a time series chart with range annotations configured to render +/// labels in the chart margin area. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class TimeSeriesRangeAnnotationMarginChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesRangeAnnotationMarginChart(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesRangeAnnotationMarginChart.withSampleData() { + return new TimeSeriesRangeAnnotationMarginChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesRangeAnnotationMarginChart.withRandomData() { + return new TimeSeriesRangeAnnotationMarginChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + // Fix one of the points to 100 so that the annotations are consistently + // placed. + new TimeSeriesSales(new DateTime(2017, 10, 10), 100), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart(seriesList, + animate: animate, + + // Allow enough space in the left and right chart margins for the + // annotations. + layoutConfig: new charts.LayoutConfig( + leftMarginSpec: new charts.MarginSpec.fixedPixel(60), + topMarginSpec: new charts.MarginSpec.fixedPixel(20), + rightMarginSpec: new charts.MarginSpec.fixedPixel(60), + bottomMarginSpec: new charts.MarginSpec.fixedPixel(20)), + behaviors: [ + // Define one domain and two measure annotations configured to render + // labels in the chart margins. + new charts.RangeAnnotation([ + new charts.RangeAnnotationSegment( + new DateTime(2017, 10, 4), + new DateTime(2017, 10, 15), + charts.RangeAnnotationAxisType.domain, + startLabel: 'D1 Start', + endLabel: 'D1 End', + labelAnchor: charts.AnnotationLabelAnchor.end, + color: charts.MaterialPalette.gray.shade200, + // Override the default vertical direction for domain labels. + labelDirection: charts.AnnotationLabelDirection.horizontal), + new charts.RangeAnnotationSegment( + 15, 20, charts.RangeAnnotationAxisType.measure, + startLabel: 'M1 Start', + endLabel: 'M1 End', + labelAnchor: charts.AnnotationLabelAnchor.end, + color: charts.MaterialPalette.gray.shade300), + new charts.RangeAnnotationSegment( + 35, 65, charts.RangeAnnotationAxisType.measure, + startLabel: 'M2 Start', + endLabel: 'M2 End', + labelAnchor: charts.AnnotationLabelAnchor.start, + color: charts.MaterialPalette.gray.shade300), + ], defaultLabelPosition: charts.AnnotationLabelPosition.margin), + ]); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/example/lib/time_series_chart/simple.dart b/example/lib/time_series_chart/simple.dart new file mode 100644 index 0000000..bf66db0 --- /dev/null +++ b/example/lib/time_series_chart/simple.dart @@ -0,0 +1,108 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Timeseries chart example +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class SimpleTimeSeriesChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + SimpleTimeSeriesChart(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory SimpleTimeSeriesChart.withSampleData() { + return new SimpleTimeSeriesChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory SimpleTimeSeriesChart.withRandomData() { + return new SimpleTimeSeriesChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 26), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 10, 10), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 19), 5), + new TimeSeriesSales(new DateTime(2017, 9, 26), 25), + new TimeSeriesSales(new DateTime(2017, 10, 3), 100), + new TimeSeriesSales(new DateTime(2017, 10, 10), 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/example/lib/time_series_chart/symbol_annotation.dart b/example/lib/time_series_chart/symbol_annotation.dart new file mode 100644 index 0000000..e57f584 --- /dev/null +++ b/example/lib/time_series_chart/symbol_annotation.dart @@ -0,0 +1,298 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of timeseries chart with annotation rows between the chart draw area +/// and the domain axis. +/// +/// The symbol annotation renderer draws a row of symbols for each series below +/// the drawArea but above the bottom axis. +/// +/// This renderer can draw point annotations and range annotations. Point +/// annotations are drawn at the location of the domain along the chart's domain +/// axis, in the row for its series. Range annotations are drawn as a range +/// shape between the domainLowerBound and domainUpperBound positions along the +/// chart's domain axis. Point annotations are drawn on top of range +/// annotations. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class TimeSeriesSymbolAnnotationChart extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesSymbolAnnotationChart(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesSymbolAnnotationChart.withSampleData() { + return new TimeSeriesSymbolAnnotationChart( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesSymbolAnnotationChart.withRandomData() { + return new TimeSeriesSymbolAnnotationChart(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final myDesktopData = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 19), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 26), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 10, 3), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 10, 10), sales: random.nextInt(100)), + ]; + + final myTabletData = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 19), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 26), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 10, 3), sales: random.nextInt(100)), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 10, 10), sales: random.nextInt(100)), + ]; + + // Example of a series with two range annotations. A regular point shape + // will be drawn at the current domain value, and a range shape will be + // drawn between the previous and target domain values. + // + // Note that these series do not contain any measure values. They are + // positioned automatically in rows. + final myAnnotationDataTop = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 24), + timePrevious: new DateTime(2017, 9, 19), + timeTarget: new DateTime(2017, 9, 24), + ), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 29), + timePrevious: new DateTime(2017, 9, 29), + timeTarget: new DateTime(2017, 10, 4), + ), + ]; + + // Example of a series with one range annotation and two single point + // annotations. Omitting the previous and target domain values causes that + // datum to be drawn as a single point. + final myAnnotationDataBottom = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 25), + timePrevious: new DateTime(2017, 9, 21), + timeTarget: new DateTime(2017, 9, 25), + ), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 31)), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 5)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: myDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: myTabletData, + ), + new charts.Series( + id: 'Annotation Series 1', + colorFn: (_, __) => charts.MaterialPalette.gray.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + domainLowerBoundFn: (TimeSeriesSales row, _) => row.timePrevious, + domainUpperBoundFn: (TimeSeriesSales row, _) => row.timeTarget, + // No measure values are needed for symbol annotations. + measureFn: (_, __) => null, + data: myAnnotationDataTop, + ) + // Configure our custom symbol annotation renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customSymbolAnnotation') + // Optional radius for the annotation shape. If not specified, this will + // default to the same radius as the points. + ..setAttribute(charts.boundsLineRadiusPxKey, 3.5), + new charts.Series( + id: 'Annotation Series 2', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + domainLowerBoundFn: (TimeSeriesSales row, _) => row.timePrevious, + domainUpperBoundFn: (TimeSeriesSales row, _) => row.timeTarget, + // No measure values are needed for symbol annotations. + measureFn: (_, __) => null, + data: myAnnotationDataBottom, + ) + // Configure our custom symbol annotation renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customSymbolAnnotation') + // Optional radius for the annotation shape. If not specified, this will + // default to the same radius as the points. + ..setAttribute(charts.boundsLineRadiusPxKey, 3.5), + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Custom renderer configuration for the point series. + customSeriesRenderers: [ + new charts.SymbolAnnotationRendererConfig( + // ID used to link series to this renderer. + customRendererId: 'customSymbolAnnotation') + ], + // Optionally pass in a [DateTimeFactory] used by the chart. The factory + // should create the same type of [DateTime] as the data provided. If none + // specified, the default creates local date time. + dateTimeFactory: const charts.LocalDateTimeFactory(), + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final myDesktopData = [ + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 19), sales: 5), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 26), sales: 25), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 3), sales: 100), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 10), sales: 75), + ]; + + final myTabletData = [ + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 19), sales: 10), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 26), sales: 50), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 3), sales: 200), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 10), sales: 150), + ]; + + // Example of a series with two range annotations. A regular point shape + // will be drawn at the current domain value, and a range shape will be + // drawn between the previous and target domain values. + // + // Note that these series do not contain any measure values. They are + // positioned automatically in rows. + final myAnnotationDataTop = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 24), + timePrevious: new DateTime(2017, 9, 19), + timeTarget: new DateTime(2017, 9, 24), + ), + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 29), + timePrevious: new DateTime(2017, 9, 29), + timeTarget: new DateTime(2017, 10, 4), + ), + ]; + + // Example of a series with one range annotation and two single point + // annotations. Omitting the previous and target domain values causes that + // datum to be drawn as a single point. + final myAnnotationDataBottom = [ + new TimeSeriesSales( + timeCurrent: new DateTime(2017, 9, 25), + timePrevious: new DateTime(2017, 9, 21), + timeTarget: new DateTime(2017, 9, 25), + ), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 9, 31)), + new TimeSeriesSales(timeCurrent: new DateTime(2017, 10, 5)), + ]; + + return [ + new charts.Series( + id: 'Desktop', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: myDesktopData, + ), + new charts.Series( + id: 'Tablet', + colorFn: (_, __) => charts.MaterialPalette.green.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: myTabletData, + ), + new charts.Series( + id: 'Annotation Series 1', + colorFn: (_, __) => charts.MaterialPalette.gray.shadeDefault, + // A point shape will be drawn at the location of the domain. + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + // A range shape will be drawn between the lower and upper domain + // bounds. The range will be drawn underneath the domain point. + domainLowerBoundFn: (TimeSeriesSales row, _) => row.timePrevious, + domainUpperBoundFn: (TimeSeriesSales row, _) => row.timeTarget, + // No measure values are needed for symbol annotations. + measureFn: (_, __) => null, + data: myAnnotationDataTop, + ) + // Configure our custom symbol annotation renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customSymbolAnnotation') + // Optional radius for the annotation range. If not specified, this will + // default to the same radius as the domain point. + ..setAttribute(charts.boundsLineRadiusPxKey, 3.5), + new charts.Series( + id: 'Annotation Series 2', + colorFn: (_, __) => charts.MaterialPalette.red.shadeDefault, + // A point shape will be drawn at the location of the domain. + domainFn: (TimeSeriesSales sales, _) => sales.timeCurrent, + // A range shape will be drawn between the lower and upper domain + // bounds. The range will be drawn underneath the domain point. + domainLowerBoundFn: (TimeSeriesSales row, _) => row.timePrevious, + domainUpperBoundFn: (TimeSeriesSales row, _) => row.timeTarget, + // No measure values are needed for symbol annotations. + measureFn: (_, __) => null, + data: myAnnotationDataBottom, + ) + // Configure our custom symbol annotation renderer for this series. + ..setAttribute(charts.rendererIdKey, 'customSymbolAnnotation') + // Optional radius for the annotation range. If not specified, this will + // default to the same radius as the domain point. + ..setAttribute(charts.boundsLineRadiusPxKey, 3.5), + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime timeCurrent; + final DateTime? timePrevious; + final DateTime? timeTarget; + final int? sales; + + TimeSeriesSales({ + required this.timeCurrent, + this.timePrevious, + this.timeTarget, + this.sales, + }); +} diff --git a/example/lib/time_series_chart/time_series_gallery.dart b/example/lib/time_series_chart/time_series_gallery.dart new file mode 100644 index 0000000..f60d6dd --- /dev/null +++ b/example/lib/time_series_chart/time_series_gallery.dart @@ -0,0 +1,80 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import '../gallery_scaffold.dart'; +import 'confidence_interval.dart'; +import 'end_points_axis.dart'; +import 'line_annotation.dart'; +import 'range_annotation.dart'; +import 'range_annotation_margin.dart'; +import 'simple.dart'; +import 'symbol_annotation.dart'; +import 'with_bar_renderer.dart'; + +List buildGallery() { + return [ + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Time Series Chart', + subtitle: 'Simple single time series chart', + childBuilder: () => new SimpleTimeSeriesChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'End Points Axis Time Series Chart', + subtitle: 'Time series chart with an end points axis', + childBuilder: () => new EndPointsAxisTimeSeriesChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Line Annotation on Time Series Chart', + subtitle: 'Time series chart with future line annotation', + childBuilder: () => new TimeSeriesLineAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Range Annotation on Time Series Chart', + subtitle: 'Time series chart with future range annotation', + childBuilder: () => new TimeSeriesRangeAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Range Annotation Margin Labels on Time Series Chart', + subtitle: + 'Time series chart with range annotations with labels in margins', + childBuilder: () => + new TimeSeriesRangeAnnotationMarginChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Symbol Annotation Time Series Chart', + subtitle: 'Time series chart with annotation data below the draw area', + childBuilder: () => new TimeSeriesSymbolAnnotationChart.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Time Series Chart with Bars', + subtitle: 'Time series chart using the bar renderer', + childBuilder: () => new TimeSeriesBar.withRandomData(), + ), + new GalleryScaffold( + listTileIcon: new Icon(Icons.show_chart), + title: 'Time Series Chart with Confidence Interval', + subtitle: 'Draws area around the confidence interval', + childBuilder: () => new TimeSeriesConfidenceInterval.withRandomData(), + ), + ]; +} diff --git a/example/lib/time_series_chart/with_bar_renderer.dart b/example/lib/time_series_chart/with_bar_renderer.dart new file mode 100644 index 0000000..c2f5ae5 --- /dev/null +++ b/example/lib/time_series_chart/with_bar_renderer.dart @@ -0,0 +1,148 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Example of a time series chart using a bar renderer. +// EXCLUDE_FROM_GALLERY_DOCS_START +import 'dart:math'; +// EXCLUDE_FROM_GALLERY_DOCS_END +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:flutter/material.dart'; + +class TimeSeriesBar extends StatelessWidget { + final List> seriesList; + final bool animate; + + TimeSeriesBar(this.seriesList, {this.animate = false}); + + /// Creates a [TimeSeriesChart] with sample data and no transition. + factory TimeSeriesBar.withSampleData() { + return new TimeSeriesBar( + _createSampleData(), + // Disable animations for image tests. + animate: false, + ); + } + + // EXCLUDE_FROM_GALLERY_DOCS_START + // This section is excluded from being copied to the gallery. + // It is used for creating random series data to demonstrate animation in + // the example app only. + factory TimeSeriesBar.withRandomData() { + return new TimeSeriesBar(_createRandomData()); + } + + /// Create random data. + static List> _createRandomData() { + final random = new Random(); + + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 1), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 2), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 3), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 4), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 5), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 6), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 7), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 8), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 9), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 10), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 11), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 12), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 13), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 14), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 15), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 16), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 17), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 18), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 19), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 20), random.nextInt(100)), + new TimeSeriesSales(new DateTime(2017, 9, 21), random.nextInt(100)), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } + // EXCLUDE_FROM_GALLERY_DOCS_END + + @override + Widget build(BuildContext context) { + return new charts.TimeSeriesChart( + seriesList, + animate: animate, + // Set the default renderer to a bar renderer. + // This can also be one of the custom renderers of the time series chart. + defaultRenderer: new charts.BarRendererConfig(), + // It is recommended that default interactions be turned off if using bar + // renderer, because the line point highlighter is the default for time + // series chart. + defaultInteractions: false, + // If default interactions were removed, optionally add select nearest + // and the domain highlighter that are typical for bar charts. + behaviors: [new charts.SelectNearest(), new charts.DomainHighlighter()], + ); + } + + /// Create one series with sample hard coded data. + static List> _createSampleData() { + final data = [ + new TimeSeriesSales(new DateTime(2017, 9, 1), 5), + new TimeSeriesSales(new DateTime(2017, 9, 2), 5), + new TimeSeriesSales(new DateTime(2017, 9, 3), 25), + new TimeSeriesSales(new DateTime(2017, 9, 4), 100), + new TimeSeriesSales(new DateTime(2017, 9, 5), 75), + new TimeSeriesSales(new DateTime(2017, 9, 6), 88), + new TimeSeriesSales(new DateTime(2017, 9, 7), 65), + new TimeSeriesSales(new DateTime(2017, 9, 8), 91), + new TimeSeriesSales(new DateTime(2017, 9, 9), 100), + new TimeSeriesSales(new DateTime(2017, 9, 10), 111), + new TimeSeriesSales(new DateTime(2017, 9, 11), 90), + new TimeSeriesSales(new DateTime(2017, 9, 12), 50), + new TimeSeriesSales(new DateTime(2017, 9, 13), 40), + new TimeSeriesSales(new DateTime(2017, 9, 14), 30), + new TimeSeriesSales(new DateTime(2017, 9, 15), 40), + new TimeSeriesSales(new DateTime(2017, 9, 16), 50), + new TimeSeriesSales(new DateTime(2017, 9, 17), 30), + new TimeSeriesSales(new DateTime(2017, 9, 18), 35), + new TimeSeriesSales(new DateTime(2017, 9, 19), 40), + new TimeSeriesSales(new DateTime(2017, 9, 20), 32), + new TimeSeriesSales(new DateTime(2017, 9, 21), 31), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (TimeSeriesSales sales, _) => sales.time, + measureFn: (TimeSeriesSales sales, _) => sales.sales, + data: data, + ) + ]; + } +} + +/// Sample time series data type. +class TimeSeriesSales { + final DateTime time; + final int sales; + + TimeSeriesSales(this.time, this.sales); +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..8910916 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,18 @@ +name: example +description: Charts-Flutter Demo +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: '>=2.5.0' + +dependencies: + charts_flutter: + path: ../ + cupertino_icons: ^0.1.0 + flutter: + sdk: flutter + flutter_test: + sdk: flutter + meta: ^1.1.1 + intl: ">=0.15.2 < 0.18.0" +flutter: + uses-material-design: true diff --git a/lib/flutter.dart b/lib/flutter.dart new file mode 100644 index 0000000..609e7de --- /dev/null +++ b/lib/flutter.dart @@ -0,0 +1,199 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export 'package:charts_common/common.dart' + show + boundsLineRadiusPxFnKey, + boundsLineRadiusPxKey, + measureAxisIdKey, + pointSymbolRendererFnKey, + pointSymbolRendererIdKey, + rendererIdKey, + AnnotationLabelAnchor, + AnnotationLabelDirection, + AnnotationLabelPosition, + ArcLabelDecorator, + ArcLabelLeaderLineStyleSpec, + ArcLabelPosition, + ArcRenderer, + ArcRendererConfig, + AutoDateTimeTickFormatterSpec, + AutoDateTimeTickProviderSpec, + Axis, + AxisDirection, + AxisSpec, + BarGroupingType, + BarLabelAnchor, + BarLabelDecorator, + BarLabelPosition, + BarLaneRendererConfig, + BarRenderer, + BarRendererConfig, + BarTargetLineRenderer, + BarTargetLineRendererConfig, + BaseCartesianRenderer, + BasicDateTimeTickFormatterSpec, + BasicNumericTickFormatterSpec, + BasicNumericTickProviderSpec, + BasicOrdinalTickProviderSpec, + BasicOrdinalTickFormatterSpec, + BehaviorPosition, + BucketingAxisSpec, + BucketingNumericTickProviderSpec, + CartesianChart, + ChartCanvas, + ChartContext, + ChartTitleDirection, + CircleSymbolRenderer, + Color, + ComparisonPointsDecorator, + ConstCornerStrategy, + CornerStrategy, + CylinderSymbolRenderer, + DateTimeAxisSpec, + DateTimeEndPointsTickProviderSpec, + DateTimeExtents, + DateTimeFactory, + DateTimeTickFormatter, + DateTimeTickFormatterSpec, + DateTimeTickProviderSpec, + DayTickProviderSpec, + DomainFormatter, + EndPointsTimeAxisSpec, + ExploreModeTrigger, + FillPatternType, + GestureListener, + GraphicsFactory, + GridlineRendererSpec, + ImmutableSeries, + InsideJustification, + LayoutPosition, + LayoutViewPaintOrder, + LayoutViewPositionOrder, + LegendDefaultMeasure, + LegendTapHandling, + LineAnnotationSegment, + LinePointHighlighterFollowLineType, + LineRenderer, + LineRendererConfig, + LineStyleSpec, + LocalDateTimeFactory, + LockSelection, + MarginSpec, + MaterialPalette, + MaterialStyle, + MaxWidthStrategy, + MeasureFormatter, + NoCornerStrategy, + NoneRenderSpec, + NumericAxis, + NumericAxisSpec, + NumericCartesianChart, + NumericEndPointsTickProviderSpec, + NumericExtents, + NumericTickFormatterSpec, + NumericTickProviderSpec, + OrdinalAxis, + OrdinalAxisSpec, + OrdinalCartesianChart, + OrdinalTickFormatterSpec, + OrdinalTickProviderSpec, + OrdinalViewport, + OutsideJustification, + PanningCompletedCallback, + PercentAxisSpec, + PercentInjectorTotalType, + Performance, + PointRenderer, + PointRendererConfig, + PointRendererDecorator, + PointRendererElement, + PointSymbolRenderer, + QuantumPalette, + RangeAnnotationAxisType, + RangeAnnotationSegment, + RectSymbolRenderer, + RenderSpec, + RTLSpec, + SelectionModel, + SelectionModelListener, + SelectionModelType, + SelectionTrigger, + Series, + SeriesDatum, + SeriesDatumConfig, + SeriesRenderer, + SeriesRendererConfig, + SimpleTickFormatterBase, + SliderListenerCallback, + SliderListenerDragState, + SliderStyle, + SmallTickRendererSpec, + StaticDateTimeTickProviderSpec, + StaticNumericTickProviderSpec, + StaticOrdinalTickProviderSpec, + StyleFactory, + SymbolAnnotationRenderer, + SymbolAnnotationRendererConfig, + TextDirection, + TextElement, + TextStyle, + TextStyleSpec, + TickFormatter, + TickFormatterSpec, + TickLabelAnchor, + TickLabelJustification, + TickSpec, + TimeFormatterSpec, + TypedAccessorFn, + UTCDateTimeFactory, + ViewMargin, + VocalizationCallback; + +export 'src/bar_chart.dart'; +export 'src/base_chart.dart' show BaseChart, LayoutConfig; +export 'src/behaviors/a11y/domain_a11y_explore_behavior.dart' + show DomainA11yExploreBehavior; +export 'src/behaviors/chart_behavior.dart' show ChartBehavior; +export 'src/behaviors/domain_highlighter.dart' show DomainHighlighter; +export 'src/behaviors/initial_selection.dart' show InitialSelection; +export 'src/behaviors/calculation/percent_injector.dart' show PercentInjector; +export 'src/behaviors/chart_title/chart_title.dart' show ChartTitle; +export 'src/behaviors/legend/datum_legend.dart' show DatumLegend; +export 'src/behaviors/legend/legend_content_builder.dart' + show LegendContentBuilder, TabularLegendContentBuilder; +export 'src/behaviors/legend/legend_entry_layout.dart' + show LegendEntryLayout, SimpleLegendEntryLayout; +export 'src/behaviors/legend/legend_layout.dart' + show LegendLayout, TabularLegendLayout; +export 'src/behaviors/legend/series_legend.dart' show SeriesLegend; +export 'src/behaviors/line_point_highlighter.dart' show LinePointHighlighter; +export 'src/behaviors/range_annotation.dart' show RangeAnnotation; +export 'src/behaviors/select_nearest.dart' show SelectNearest; +export 'src/behaviors/sliding_viewport.dart' show SlidingViewport; +export 'src/behaviors/slider/slider.dart' show Slider; +export 'src/behaviors/zoom/initial_hint_behavior.dart' show InitialHintBehavior; +export 'src/behaviors/zoom/pan_and_zoom_behavior.dart' show PanAndZoomBehavior; +export 'src/behaviors/zoom/pan_behavior.dart' show PanBehavior; +export 'src/combo_chart/combo_chart.dart'; +export 'src/line_chart.dart'; +export 'src/pie_chart.dart'; +export 'src/scatter_plot_chart.dart'; +export 'src/selection_model_config.dart' show SelectionModelConfig; +export 'src/symbol_renderer.dart' show CustomSymbolRenderer; +export 'src/time_series_chart.dart'; +export 'src/user_managed_state.dart' + show UserManagedState, UserManagedSelectionModel; +export 'src/util/color.dart' show ColorUtil; diff --git a/lib/src/bar_chart.dart b/lib/src/bar_chart.dart new file mode 100644 index 0000000..db154d0 --- /dev/null +++ b/lib/src/bar_chart.dart @@ -0,0 +1,104 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection' show LinkedHashMap; + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + BarChart, + BarGroupingType, + BarRendererConfig, + BarRendererDecorator, + NumericAxisSpec, + RTLSpec, + Series, + SeriesRendererConfig; +import 'behaviors/domain_highlighter.dart' show DomainHighlighter; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'package:meta/meta.dart' show immutable; +import 'base_chart.dart' show LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'cartesian_chart.dart' show CartesianChart; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +@immutable +class BarChart extends CartesianChart { + final bool vertical; + final common.BarRendererDecorator? barRendererDecorator; + + BarChart( + List> seriesList, { + bool? animate, + Duration? animationDuration, + common.AxisSpec? domainAxis, + common.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? secondaryMeasureAxis, + LinkedHashMap? disjointMeasureAxes, + common.BarGroupingType? barGroupingType, + common.BarRendererConfig? defaultRenderer, + List>? customSeriesRenderers, + List>? behaviors, + List>? selectionModels, + common.RTLSpec? rtlSpec, + this.vertical = true, + bool defaultInteractions = true, + LayoutConfig? layoutConfig, + UserManagedState? userManagedState, + this.barRendererDecorator, + bool? flipVerticalAxis, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes, + defaultRenderer: defaultRenderer ?? + new common.BarRendererConfig( + groupingType: barGroupingType, + barRendererDecorator: barRendererDecorator), + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + defaultInteractions: defaultInteractions, + layoutConfig: layoutConfig, + userManagedState: userManagedState, + flipVerticalAxis: flipVerticalAxis, + ); + + @override + common.BarChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.BarChart( + vertical: vertical, + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(), + disjointMeasureAxes: createDisjointMeasureAxes()); + } + + @override + void addDefaultInteractions(List behaviors) { + super.addDefaultInteractions(behaviors); + + behaviors.add(new DomainHighlighter()); + } +} diff --git a/lib/src/base_chart.dart b/lib/src/base_chart.dart new file mode 100644 index 0000000..aafe57a --- /dev/null +++ b/lib/src/base_chart.dart @@ -0,0 +1,279 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show + BaseChart, + LayoutConfig, + MarginSpec, + Performance, + RTLSpec, + Series, + SeriesRendererConfig, + SelectionModelType, + SelectionTrigger; +import 'behaviors/select_nearest.dart' show SelectNearest; +import 'package:meta/meta.dart' show immutable; +import 'behaviors/chart_behavior.dart' + show ChartBehavior, ChartStateBehavior, GestureType; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'package:flutter/material.dart' show StatefulWidget; +import 'base_chart_state.dart' show BaseChartState; +import 'user_managed_state.dart' show UserManagedState; + +@immutable +abstract class BaseChart extends StatefulWidget { + /// Series list to draw. + final List> seriesList; + + /// Animation transitions. + final bool animate; + final Duration animationDuration; + + /// Used to configure the margin sizes around the drawArea that the axis and + /// other things render into. + final LayoutConfig? layoutConfig; + + // Default renderer used to draw series data on the chart. + final common.SeriesRendererConfig? defaultRenderer; + + /// Include the default interactions or not. + final bool defaultInteractions; + + final List>? behaviors; + + final List>? selectionModels; + + // List of custom series renderers used to draw series data on the chart. + // + // Series assigned a rendererIdKey will be drawn with the matching renderer in + // this list. Series without a rendererIdKey will be drawn by the default + // renderer. + final List>? customSeriesRenderers; + + /// The spec to use if RTL is enabled. + final common.RTLSpec? rtlSpec; + + /// Optional state that overrides internally kept state, such as selection. + final UserManagedState? userManagedState; + + BaseChart(this.seriesList, + {bool? animate, + Duration? animationDuration, + this.defaultRenderer, + this.customSeriesRenderers, + this.behaviors, + this.selectionModels, + this.rtlSpec, + this.defaultInteractions = true, + this.layoutConfig, + this.userManagedState}) + : this.animate = animate ?? true, + this.animationDuration = + animationDuration ?? const Duration(milliseconds: 300); + + @override + BaseChartState createState() => new BaseChartState(); + + /// Creates and returns a [common.BaseChart]. + common.BaseChart createCommonChart(BaseChartState chartState); + + /// Updates the [common.BaseChart]. + void updateCommonChart(common.BaseChart chart, BaseChart? oldWidget, + BaseChartState chartState) { + common.Performance.time('chartsUpdateRenderers'); + // Set default renderer if one was provided. + if (defaultRenderer != null && + defaultRenderer != oldWidget?.defaultRenderer) { + chart.defaultRenderer = defaultRenderer!.build(); + chartState.markChartDirty(); + } + + // Add custom series renderers if any were provided. + if (customSeriesRenderers != null) { + // TODO: This logic does not remove old renderers and + // shouldn't require the series configs to remain in the same order. + for (var i = 0; i < customSeriesRenderers!.length; i++) { + if (oldWidget == null || + (oldWidget.customSeriesRenderers != null && + i > oldWidget.customSeriesRenderers!.length) || + customSeriesRenderers![i] != oldWidget.customSeriesRenderers![i]) { + chart.addSeriesRenderer(customSeriesRenderers![i].build()); + chartState.markChartDirty(); + } + } + } + common.Performance.timeEnd('chartsUpdateRenderers'); + + common.Performance.time('chartsUpdateBehaviors'); + _updateBehaviors(chart, chartState); + common.Performance.timeEnd('chartsUpdateBehaviors'); + + _updateSelectionModel(chart, chartState); + + chart.transition = animate ? animationDuration : Duration.zero; + } + + void _updateBehaviors(common.BaseChart chart, BaseChartState chartState) { + final behaviorList = List>.from(behaviors ?? []); + + // Insert automatic behaviors to the front of the behavior list. + if (defaultInteractions) { + if (chartState.autoBehaviorWidgets.isEmpty) { + addDefaultInteractions(chartState.autoBehaviorWidgets); + } + + // Add default interaction behaviors to the front of the list if they + // don't conflict with user behaviors by role. + chartState.autoBehaviorWidgets.reversed + .where(_notACustomBehavior) + .forEach((ChartBehavior behavior) { + behaviorList.insert(0, behavior); + }); + } + + // Remove any behaviors from the chart that are not in the incoming list. + // Walk in reverse order they were added. + // Also, remove any persisting behaviors from incoming list. + for (int i = chartState.addedBehaviorWidgets.length - 1; i >= 0; i--) { + final addedBehavior = chartState.addedBehaviorWidgets[i]; + if (!behaviorList.remove(addedBehavior)) { + final role = addedBehavior.role; + chartState.addedBehaviorWidgets.remove(addedBehavior); + chartState.addedCommonBehaviorsByRole.remove(role); + chart.removeBehavior(chartState.addedCommonBehaviorsByRole[role]); + chartState.markChartDirty(); + } + } + + // Add any remaining/new behaviors. + behaviorList.forEach((ChartBehavior behaviorWidget) { + final commonBehavior = behaviorWidget.createCommonBehavior(); + + // Assign the chart state to any behavior that needs it. + if (commonBehavior is ChartStateBehavior) { + (commonBehavior as ChartStateBehavior).chartState = chartState; + } + + chart.addBehavior(commonBehavior); + chartState.addedBehaviorWidgets.add(behaviorWidget); + chartState.addedCommonBehaviorsByRole[behaviorWidget.role] = + commonBehavior; + chartState.markChartDirty(); + }); + } + + /// Create the list of default interaction behaviors. + void addDefaultInteractions(List behaviors) { + // Update selection model + behaviors.add(new SelectNearest( + eventTrigger: common.SelectionTrigger.tap, + selectionModelType: common.SelectionModelType.info, + selectClosestSeries: true)); + } + + bool _notACustomBehavior(ChartBehavior behavior) { + return behaviors == null || + !behaviors!.any( + (ChartBehavior userBehavior) => userBehavior.role == behavior.role); + } + + void _updateSelectionModel( + common.BaseChart chart, BaseChartState chartState) { + final prevTypes = new List.from( + chartState.addedSelectionChangedListenersByType.keys); + + // Update any listeners for each type. + selectionModels?.forEach((SelectionModelConfig model) { + final selectionModel = chart.getSelectionModel(model.type); + + final prevChangedListener = + chartState.addedSelectionChangedListenersByType[model.type]; + if (!identical(model.changedListener, prevChangedListener)) { + if (prevChangedListener != null) { + selectionModel.removeSelectionChangedListener(prevChangedListener); + } + selectionModel.addSelectionChangedListener(model.changedListener!); + chartState.addedSelectionChangedListenersByType[model.type] = + model.changedListener!; + } + + final prevUpdatedListener = + chartState.addedSelectionUpdatedListenersByType[model.type]; + if (!identical(model.updatedListener, prevUpdatedListener)) { + if (prevUpdatedListener != null) { + selectionModel.removeSelectionUpdatedListener(prevUpdatedListener); + } + selectionModel.addSelectionUpdatedListener(model.updatedListener!); + chartState.addedSelectionUpdatedListenersByType[model.type] = + model.updatedListener!; + } + + prevTypes.remove(model.type); + }); + + // Remove any lingering listeners. + prevTypes.forEach((common.SelectionModelType type) { + chart.getSelectionModel(type) + ..removeSelectionChangedListener( + chartState.addedSelectionChangedListenersByType[type]!) + ..removeSelectionUpdatedListener( + chartState.addedSelectionUpdatedListenersByType[type]!); + }); + } + + /// Gets distinct set of gestures this chart will subscribe to. + /// + /// This is needed to allow setup of the [GestureDetector] widget with only + /// gestures we need to listen to and it must wrap [ChartContainer] widget. + /// Gestures are then setup to be proxied in [common.BaseChart] and that is + /// held by [ChartContainerRenderObject]. + Set getDesiredGestures(BaseChartState chartState) { + final types = new Set(); + behaviors?.forEach((ChartBehavior behavior) { + types.addAll(behavior.desiredGestures); + }); + + if (defaultInteractions && chartState.autoBehaviorWidgets.isEmpty) { + addDefaultInteractions(chartState.autoBehaviorWidgets); + } + + chartState.autoBehaviorWidgets.forEach((ChartBehavior behavior) { + types.addAll(behavior.desiredGestures); + }); + return types; + } +} + +@immutable +class LayoutConfig { + final common.MarginSpec leftMarginSpec; + final common.MarginSpec topMarginSpec; + final common.MarginSpec rightMarginSpec; + final common.MarginSpec bottomMarginSpec; + + LayoutConfig({ + required this.leftMarginSpec, + required this.topMarginSpec, + required this.rightMarginSpec, + required this.bottomMarginSpec, + }); + + common.LayoutConfig get commonLayoutConfig => new common.LayoutConfig( + leftSpec: leftMarginSpec, + topSpec: topMarginSpec, + rightSpec: rightMarginSpec, + bottomSpec: bottomMarginSpec); +} diff --git a/lib/src/base_chart_state.dart b/lib/src/base_chart_state.dart new file mode 100644 index 0000000..1b85334 --- /dev/null +++ b/lib/src/base_chart_state.dart @@ -0,0 +1,186 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:ui' show TextDirection; +import 'package:flutter/material.dart' + show + AnimationController, + BuildContext, + State, + TickerProviderStateMixin, + Widget; +import 'package:charts_common/common.dart' as common; +import 'package:flutter/widgets.dart' + show Directionality, LayoutId, CustomMultiChildLayout; +import 'behaviors/chart_behavior.dart' + show BuildableBehavior, ChartBehavior, ChartStateBehavior; +import 'base_chart.dart' show BaseChart; +import 'chart_container.dart' show ChartContainer; +import 'chart_state.dart' show ChartState; +import 'chart_gesture_detector.dart' show ChartGestureDetector; +import 'widget_layout_delegate.dart'; + +class BaseChartState extends State> + with TickerProviderStateMixin + implements ChartState { + // Animation + late AnimationController _animationController; + double _animationValue = 0.0; + + BaseChart? _oldWidget; + + ChartGestureDetector? _chartGestureDetector; + + bool _configurationChanged = false; + + final autoBehaviorWidgets = >[]; + final addedBehaviorWidgets = >[]; + final addedCommonBehaviorsByRole = {}; + + final addedSelectionChangedListenersByType = + >{}; + final addedSelectionUpdatedListenersByType = + >{}; + + final _behaviorAnimationControllers = + {}; + + static const chartContainerLayoutID = 'chartContainer'; + + @override + void initState() { + super.initState(); + _animationController = new AnimationController(vsync: this) + ..addListener(_animationTick); + } + + @override + void requestRebuild() { + setState(() {}); + } + + @override + void markChartDirty() { + _configurationChanged = true; + } + + @override + void resetChartDirtyFlag() { + _configurationChanged = false; + } + + @override + bool get chartIsDirty => _configurationChanged; + + @override + void setState(fn) { + if (mounted) { + super.setState(fn); + } + } + + /// Builds the common chart canvas widget. + Widget _buildChartContainer() { + final chartContainer = new ChartContainer( + oldChartWidget: _oldWidget, + chartWidget: widget, + chartState: this, + animationValue: _animationValue, + rtl: Directionality.of(context) == TextDirection.rtl, + rtlSpec: widget.rtlSpec, + userManagedState: widget.userManagedState, + ); + _oldWidget = widget; + + final desiredGestures = widget.getDesiredGestures(this); + if (desiredGestures.isNotEmpty) { + _chartGestureDetector ??= new ChartGestureDetector(); + return _chartGestureDetector! + .makeWidget(context, chartContainer, desiredGestures); + } else { + return chartContainer; + } + } + + @override + Widget build(BuildContext context) { + final chartWidgets = []; + final idAndBehaviorMap = {}; + + // Add the common chart canvas widget. + chartWidgets.add(new LayoutId( + id: chartContainerLayoutID, child: _buildChartContainer())); + + // Add widget for each behavior that can build widgets + addedCommonBehaviorsByRole.forEach((id, behavior) { + if (behavior is BuildableBehavior) { + assert(id != chartContainerLayoutID); + + final buildableBehavior = behavior as BuildableBehavior; + idAndBehaviorMap[id] = buildableBehavior; + + final widget = buildableBehavior.build(context); + chartWidgets.add(new LayoutId(id: id, child: widget)); + } + }); + + final isRTL = Directionality.of(context) == TextDirection.rtl; + + return new CustomMultiChildLayout( + delegate: new WidgetLayoutDelegate( + chartContainerLayoutID, idAndBehaviorMap, isRTL), + children: chartWidgets); + } + + @override + void dispose() { + _animationController.dispose(); + _behaviorAnimationControllers + .forEach((_, controller) => controller.dispose()); + _behaviorAnimationControllers.clear(); + super.dispose(); + } + + @override + void setAnimation(Duration transition) { + _playAnimation(transition); + } + + void _playAnimation(Duration duration) { + _animationController.duration = duration; + _animationController.forward(from: (duration == Duration.zero) ? 1.0 : 0.0); + _animationValue = _animationController.value; + } + + void _animationTick() { + setState(() { + _animationValue = _animationController.value; + }); + } + + /// Get animation controller to be used by [behavior]. + AnimationController getAnimationController(ChartStateBehavior behavior) { + _behaviorAnimationControllers[behavior] ??= + new AnimationController(vsync: this); + + return _behaviorAnimationControllers[behavior]!; + } + + /// Dispose of animation controller used by [behavior]. + void disposeAnimationController(ChartStateBehavior behavior) { + final controller = _behaviorAnimationControllers.remove(behavior); + controller?.dispose(); + } +} diff --git a/lib/src/behaviors/a11y/domain_a11y_explore_behavior.dart b/lib/src/behaviors/a11y/domain_a11y_explore_behavior.dart new file mode 100644 index 0000000..aeeebea --- /dev/null +++ b/lib/src/behaviors/a11y/domain_a11y_explore_behavior.dart @@ -0,0 +1,116 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show + ChartBehavior, + DomainA11yExploreBehavior, + VocalizationCallback, + ExploreModeTrigger; +import 'package:flutter/widgets.dart' ; +import '../chart_behavior.dart' show ChartBehavior, GestureType; + +/// Behavior that generates semantic nodes for each domain. +class DomainA11yExploreBehavior extends ChartBehavior { + /// Returns a string for a11y vocalization from a list of series datum. + final common.VocalizationCallback? vocalizationCallback; + + final Set desiredGestures; + + /// The gesture that activates explore mode. Defaults to long press. + /// + /// Turning on explore mode asks this [A11yBehavior] to generate nodes within + /// this chart. + final common.ExploreModeTrigger? exploreModeTrigger; + + /// Minimum width of the bounding box for the a11y focus. + /// + /// Must be 1 or higher because invisible semantic nodes should not be added. + final double? minimumWidth; + + /// Optionally notify the OS when explore mode is enabled. + final String? exploreModeEnabledAnnouncement; + + /// Optionally notify the OS when explore mode is disabled. + final String? exploreModeDisabledAnnouncement; + + DomainA11yExploreBehavior._internal( + {this.vocalizationCallback, + this.exploreModeTrigger, + required this.desiredGestures, + this.minimumWidth, + this.exploreModeEnabledAnnouncement, + this.exploreModeDisabledAnnouncement}); + + factory DomainA11yExploreBehavior({ + common.VocalizationCallback? vocalizationCallback, + common.ExploreModeTrigger? exploreModeTrigger, + double? minimumWidth, + String? exploreModeEnabledAnnouncement, + String? exploreModeDisabledAnnouncement, + }) { + final desiredGestures = new Set(); + exploreModeTrigger ??= common.ExploreModeTrigger.pressHold; + + switch (exploreModeTrigger) { + case common.ExploreModeTrigger.pressHold: + desiredGestures..add(GestureType.onLongPress); + break; + case common.ExploreModeTrigger.tap: + desiredGestures..add(GestureType.onTap); + break; + } + + return new DomainA11yExploreBehavior._internal( + vocalizationCallback: vocalizationCallback, + desiredGestures: desiredGestures, + exploreModeTrigger: exploreModeTrigger, + minimumWidth: minimumWidth, + exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement, + exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement, + ); + } + + @override + common.DomainA11yExploreBehavior createCommonBehavior() { + return new common.DomainA11yExploreBehavior( + vocalizationCallback: vocalizationCallback, + exploreModeTrigger: exploreModeTrigger, + minimumWidth: minimumWidth, + exploreModeEnabledAnnouncement: exploreModeEnabledAnnouncement, + exploreModeDisabledAnnouncement: exploreModeDisabledAnnouncement); + } + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'DomainA11yExplore-${exploreModeTrigger}'; + + @override + bool operator ==(Object o) => + o is DomainA11yExploreBehavior && + vocalizationCallback == o.vocalizationCallback && + exploreModeTrigger == o.exploreModeTrigger && + minimumWidth == o.minimumWidth && + exploreModeEnabledAnnouncement == o.exploreModeEnabledAnnouncement && + exploreModeDisabledAnnouncement == o.exploreModeDisabledAnnouncement; + + @override + int get hashCode { + return Object.hash(minimumWidth, vocalizationCallback, exploreModeTrigger, + exploreModeEnabledAnnouncement, exploreModeDisabledAnnouncement); + } +} diff --git a/lib/src/behaviors/calculation/percent_injector.dart b/lib/src/behaviors/calculation/percent_injector.dart new file mode 100644 index 0000000..f2038f7 --- /dev/null +++ b/lib/src/behaviors/calculation/percent_injector.dart @@ -0,0 +1,67 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show ChartBehavior, PercentInjector, PercentInjectorTotalType; +import 'package:meta/meta.dart' show immutable; + +import '../chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that can inject series or domain percentages into each datum. +/// +/// [totalType] configures the type of total to be calculated. +/// +/// The measure values of each datum will be replaced by the percent of the +/// total measure value that each represents. The "raw" measure accessor +/// function on [MutableSeries] can still be used to get the original values. +/// +/// Note that the results for measureLowerBound and measureUpperBound are not +/// currently well defined when converted into percentage values. This behavior +/// will replace them as percents to prevent bad axis results, but no effort is +/// made to bound them to within a "0 to 100%" data range. +/// +/// Note that if the chart has a [Legend] that is capable of hiding series data, +/// then this behavior must be added after the [Legend] to ensure that it +/// calculates values after series have been potentially removed from the list. +@immutable +class PercentInjector extends ChartBehavior { + final desiredGestures = new Set(); + + /// The type of data total to be calculated. + final common.PercentInjectorTotalType totalType; + + /// Constructs a [PercentInjector]. + /// + /// [totalType] configures the type of data total to be calculated. + PercentInjector({this.totalType = common.PercentInjectorTotalType.domain}); + + @override + common.PercentInjector createCommonBehavior() => + new common.PercentInjector(totalType: totalType); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'PercentInjector'; + + @override + bool operator ==(Object o) { + return o is PercentInjector && totalType == o.totalType; + } + + @override + int get hashCode => totalType.hashCode; +} diff --git a/lib/src/behaviors/chart_behavior.dart b/lib/src/behaviors/chart_behavior.dart new file mode 100644 index 0000000..c580416 --- /dev/null +++ b/lib/src/behaviors/chart_behavior.dart @@ -0,0 +1,72 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Rectangle; +import 'package:charts_common/common.dart' as common + show + BehaviorPosition, + InsideJustification, + OutsideJustification, + ChartBehavior; +import 'package:meta/meta.dart' show immutable; +import 'package:flutter/widgets.dart' show BuildContext, Widget; + +import '../base_chart_state.dart' show BaseChartState; + +/// Flutter wrapper for chart behaviors. +@immutable +abstract class ChartBehavior { + Set get desiredGestures; + + common.ChartBehavior createCommonBehavior(); + + void updateCommonBehavior(common.ChartBehavior commonBehavior); + + String get role; +} + +/// A chart behavior that depends on Flutter [State]. +abstract class ChartStateBehavior { + set chartState(BaseChartState chartState); +} + +/// A chart behavior that can build a Flutter [Widget]. +abstract class BuildableBehavior { + /// Builds a [Widget] based on the information passed in. + /// + /// [context] Flutter build context for extracting inherited properties such + /// as Directionality. + Widget build(BuildContext context); + + /// The position on the widget. + common.BehaviorPosition get position; + + /// Justification of the widget, if [position] is top, bottom, start, or end. + common.OutsideJustification get outsideJustification; + + /// Justification of the widget if [position] is [common.BehaviorPosition.inside]. + common.InsideJustification get insideJustification; + + /// Chart's draw area bounds are used for positioning. + Rectangle? get drawAreaBounds; +} + +/// Types of gestures accepted by a chart. +enum GestureType { + onLongPress, + onTap, + onHover, + onDrag, +} diff --git a/lib/src/behaviors/chart_title/chart_title.dart b/lib/src/behaviors/chart_title/chart_title.dart new file mode 100644 index 0000000..014cee0 --- /dev/null +++ b/lib/src/behaviors/chart_title/chart_title.dart @@ -0,0 +1,202 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show + BehaviorPosition, + ChartBehavior, + ChartTitle, + ChartTitleDirection, + MaxWidthStrategy, + OutsideJustification, + TextStyleSpec; +import 'package:flutter/widgets.dart' ; +import 'package:meta/meta.dart' show immutable; + +import '../chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that adds a ChartTitle widget to a chart. +@immutable +class ChartTitle extends ChartBehavior { + final desiredGestures = new Set(); + + final common.BehaviorPosition? behaviorPosition; + + /// Minimum size of the legend component. Optional. + /// + /// If the legend is positioned in the top or bottom margin, then this + /// configures the legend's height. If positioned in the start or end + /// position, this configures the legend's width. + final int? layoutMinSize; + + /// Preferred size of the legend component. Defaults to 0. + /// + /// If the legend is positioned in the top or bottom margin, then this + /// configures the legend's height. If positioned in the start or end + /// position, this configures the legend's width. + final int? layoutPreferredSize; + + /// Strategy for handling title text that is too large to fit. Defaults to + /// truncating the text with ellipses. + final common.MaxWidthStrategy? maxWidthStrategy; + + /// Primary text for the title. + final String title; + + /// Direction of the chart title text. + /// + /// This defaults to horizontal for a title in the top or bottom + /// [behaviorPosition], or vertical for start or end [behaviorPosition]. + final common.ChartTitleDirection? titleDirection; + + /// Justification of the title text if it is positioned outside of the draw + /// area. + final common.OutsideJustification? titleOutsideJustification; + + /// Space between the title and sub-title text, if defined. + /// + /// This padding is not used if no sub-title is provided. + final int? titlePadding; + + /// Style of the [title] text. + final common.TextStyleSpec? titleStyleSpec; + + /// Secondary text for the sub-title. + /// + /// [subTitle] is rendered on a second line below the [title], and may be + /// styled differently. + final String? subTitle; + + /// Style of the [subTitle] text. + final common.TextStyleSpec? subTitleStyleSpec; + + /// Space between the "inside" of the chart, and the title behavior itself. + /// + /// This padding is applied to all the edge of the title that is in the + /// direction of the draw area. For a top positioned title, this is applied + /// to the bottom edge. [outerPadding] is applied to the top, left, and right + /// edges. + /// + /// If a sub-title is defined, this is the space between the sub-title text + /// and the inside of the chart. Otherwise, it is the space between the title + /// text and the inside of chart. + final int? innerPadding; + + /// Space between the "outside" of the chart, and the title behavior itself. + /// + /// This padding is applied to all 3 edges of the title that are not in the + /// direction of the draw area. For a top positioned title, this is applied + /// to the top, left, and right edges. [innerPadding] is applied to the + /// bottom edge. + final int? outerPadding; + + /// Constructs a [ChartTitle]. + /// + /// [title] primary text for the title. + /// + /// [behaviorPosition] layout position for the title. Defaults to the top of + /// the chart. + /// + /// [innerPadding] space between the "inside" of the chart, and the title + /// behavior itself. + /// + /// [maxWidthStrategy] strategy for handling title text that is too large to + /// fit. Defaults to truncating the text with ellipses. + /// + /// [titleDirection] direction of the chart title text. + /// + /// [titleOutsideJustification] Justification of the title text if it is + /// positioned outside of the draw. Defaults to the middle of the margin area. + /// + /// [titlePadding] space between the title and sub-title text, if defined. + /// + /// [titleStyleSpec] style of the [title] text. + /// + /// [subTitle] secondary text for the sub-title. Optional. + /// + /// [subTitleStyleSpec] style of the [subTitle] text. + ChartTitle( + this.title, { + this.behaviorPosition, + this.innerPadding, + this.layoutMinSize, + this.layoutPreferredSize, + this.outerPadding, + this.maxWidthStrategy, + this.titleDirection, + this.titleOutsideJustification, + this.titlePadding, + this.titleStyleSpec, + this.subTitle, + this.subTitleStyleSpec, + }); + + @override + common.ChartTitle createCommonBehavior() => new common.ChartTitle(title, + behaviorPosition: behaviorPosition, + innerPadding: innerPadding, + layoutMinSize: layoutMinSize, + layoutPreferredSize: layoutPreferredSize, + outerPadding: outerPadding, + maxWidthStrategy: maxWidthStrategy, + titleDirection: titleDirection, + titleOutsideJustification: titleOutsideJustification, + titlePadding: titlePadding, + titleStyleSpec: titleStyleSpec, + subTitle: subTitle, + subTitleStyleSpec: subTitleStyleSpec); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'ChartTitle-${behaviorPosition.toString()}'; + + @override + bool operator ==(Object o) { + return o is ChartTitle && + behaviorPosition == o.behaviorPosition && + layoutMinSize == o.layoutMinSize && + layoutPreferredSize == o.layoutPreferredSize && + maxWidthStrategy == o.maxWidthStrategy && + title == o.title && + titleDirection == o.titleDirection && + titleOutsideJustification == o.titleOutsideJustification && + titleStyleSpec == o.titleStyleSpec && + subTitle == o.subTitle && + subTitleStyleSpec == o.subTitleStyleSpec && + innerPadding == o.innerPadding && + titlePadding == o.titlePadding && + outerPadding == o.outerPadding; + } + + @override + int get hashCode { + return Object.hash( + behaviorPosition, + layoutMinSize, + layoutPreferredSize, + maxWidthStrategy, + title, + titleDirection, + titleOutsideJustification, + titleStyleSpec, + subTitle, + subTitleStyleSpec, + innerPadding, + titlePadding, + outerPadding); + } +} diff --git a/lib/src/behaviors/domain_highlighter.dart b/lib/src/behaviors/domain_highlighter.dart new file mode 100644 index 0000000..e6e2613 --- /dev/null +++ b/lib/src/behaviors/domain_highlighter.dart @@ -0,0 +1,54 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show ChartBehavior, DomainHighlighter, SelectionModelType; + +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that monitors the specified [SelectionModel] and darkens the +/// color for selected data. +/// +/// This is typically used for bars and pies to highlight segments. +/// +/// It is used in combination with SelectNearest to update the selection model +/// and expand selection out to the domain value. +@immutable +class DomainHighlighter extends ChartBehavior { + final desiredGestures = new Set(); + + final common.SelectionModelType selectionModelType; + + DomainHighlighter([this.selectionModelType = common.SelectionModelType.info]); + + @override + common.DomainHighlighter createCommonBehavior() => + new common.DomainHighlighter(selectionModelType); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'domainHighlight-${selectionModelType.toString()}'; + + @override + bool operator ==(Object o) => + o is DomainHighlighter && selectionModelType == o.selectionModelType; + + @override + int get hashCode => selectionModelType.hashCode; +} diff --git a/lib/src/behaviors/initial_selection.dart b/lib/src/behaviors/initial_selection.dart new file mode 100644 index 0000000..cb056b5 --- /dev/null +++ b/lib/src/behaviors/initial_selection.dart @@ -0,0 +1,69 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show ChartBehavior, InitialSelection, SeriesDatumConfig, SelectionModelType; +import 'package:collection/collection.dart' show ListEquality; +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that sets the initial selection for a [selectionModelType]. +@immutable +class InitialSelection extends ChartBehavior { + final desiredGestures = new Set(); + + final common.SelectionModelType selectionModelType; + final List? selectedSeriesConfig; + final List>? selectedDataConfig; + final bool shouldPreserveSelectionOnDraw; + + InitialSelection( + {this.selectionModelType = common.SelectionModelType.info, + this.selectedSeriesConfig, + this.selectedDataConfig, + this.shouldPreserveSelectionOnDraw = false}); + + @override + common.InitialSelection createCommonBehavior() => + new common.InitialSelection( + selectionModelType: selectionModelType, + selectedDataConfig: selectedDataConfig, + selectedSeriesConfig: selectedSeriesConfig, + shouldPreserveSelectionOnDraw: shouldPreserveSelectionOnDraw); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'InitialSelection-${selectionModelType.toString()}'; + + @override + bool operator ==(Object o) { + return o is InitialSelection && + selectionModelType == o.selectionModelType && + new ListEquality() + .equals(selectedSeriesConfig, o.selectedSeriesConfig) && + new ListEquality().equals(selectedDataConfig, o.selectedDataConfig); + } + + @override + int get hashCode { + int hashcode = selectionModelType.hashCode; + hashcode = hashcode * 37 + (selectedSeriesConfig?.hashCode ?? 0); + hashcode = hashcode * 37 + (selectedDataConfig?.hashCode ?? 0); + return hashcode; + } +} diff --git a/lib/src/behaviors/legend/datum_legend.dart b/lib/src/behaviors/legend/datum_legend.dart new file mode 100644 index 0000000..d0f69c6 --- /dev/null +++ b/lib/src/behaviors/legend/datum_legend.dart @@ -0,0 +1,341 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show + BehaviorPosition, + ChartBehavior, + DatumLegend, + InsideJustification, + LegendEntry, + MeasureFormatter, + LegendDefaultMeasure, + OutsideJustification, + SelectionModelType, + TextStyleSpec; +import 'package:flutter/widgets.dart' + show BuildContext, EdgeInsets, Widget, hashValues; +import 'package:meta/meta.dart' show immutable; +import '../../chart_container.dart' show ChartContainerRenderObject; +import '../chart_behavior.dart' + show BuildableBehavior, ChartBehavior, GestureType; +import 'legend.dart' show TappableLegend; +import 'legend_content_builder.dart' + show LegendContentBuilder, TabularLegendContentBuilder; +import 'legend_layout.dart' show TabularLegendLayout; + +/// Datum legend behavior for charts. +/// +/// By default this behavior creates one legend entry per datum in the first +/// series rendered on the chart. +@immutable +class DatumLegend extends ChartBehavior { + static const defaultBehaviorPosition = common.BehaviorPosition.top; + static const defaultOutsideJustification = + common.OutsideJustification.startDrawArea; + static const defaultInsideJustification = common.InsideJustification.topStart; + + final desiredGestures = new Set(); + + final common.SelectionModelType? selectionModelType; + + /// Builder for creating custom legend content. + final LegendContentBuilder contentBuilder; + + /// Position of the legend relative to the chart. + final common.BehaviorPosition position; + + /// Justification of the legend relative to the chart + final common.OutsideJustification outsideJustification; + final common.InsideJustification insideJustification; + + /// Whether or not the legend should show measures. + /// + /// By default this is false, measures are not shown. When set to true, the + /// default behavior is to show measure only if there is selected data. + /// Please set [legendDefaultMeasure] to something other than none to enable + /// showing measures when there is no selection. + /// + /// This flag is used by the [contentBuilder], so a custom content builder + /// has to choose if it wants to use this flag. + final bool showMeasures; + + /// Option to show measures when selection is null. + /// + /// By default this is set to none, so no measures are shown when there is + /// no selection. + final common.LegendDefaultMeasure? legendDefaultMeasure; + + /// Formatter for measure value(s) if the measures are shown on the legend. + final common.MeasureFormatter? measureFormatter; + + /// Formatter for secondary measure value(s) if the measures are shown on the + /// legend and the series uses the secondary axis. + final common.MeasureFormatter? secondaryMeasureFormatter; + + /// Styles for legend entry label text. + final common.TextStyleSpec? entryTextStyle; + + static const defaultCellPadding = const EdgeInsets.all(8.0); + + /// Create a new tabular layout legend. + /// + /// By default, the legend is place above the chart and horizontally aligned + /// to the start of the draw area. + /// + /// [position] the legend will be positioned relative to the chart. Default + /// position is top. + /// + /// [outsideJustification] justification of the legend relative to the chart + /// if the position is top, bottom, left, right. Default to start of the draw + /// area. + /// + /// [insideJustification] justification of the legend relative to the chart if + /// the position is inside. Default to top of the chart, start of draw area. + /// Start of draw area means left for LTR directionality, and right for RTL. + /// + /// [horizontalFirst] if true, legend entries will grow horizontally first + /// instead of vertically first. If the position is top, bottom, or inside, + /// this defaults to true. Otherwise false. + /// + /// [desiredMaxRows] the max rows to use before layout out items in a new + /// column. By default there is no limit. The max columns created is the + /// smaller of desiredMaxRows and number of legend entries. + /// + /// [desiredMaxColumns] the max columns to use before laying out items in a + /// new row. By default there is no limit. The max columns created is the + /// smaller of desiredMaxColumns and number of legend entries. + /// + /// [showMeasures] show measure values for each series. + /// + /// [legendDefaultMeasure] if measure should show when there is no selection. + /// This is set to none by default (only shows measure for selected data). + /// + /// [measureFormatter] formats measure value if measures are shown. + /// + /// [secondaryMeasureFormatter] formats measures if measures are shown for the + /// series that uses secondary measure axis. + factory DatumLegend({ + common.BehaviorPosition? position, + common.OutsideJustification? outsideJustification, + common.InsideJustification? insideJustification, + bool? horizontalFirst, + int? desiredMaxRows, + int? desiredMaxColumns, + EdgeInsets? cellPadding, + bool? showMeasures, + common.LegendDefaultMeasure? legendDefaultMeasure, + common.MeasureFormatter? measureFormatter, + common.MeasureFormatter? secondaryMeasureFormatter, + common.TextStyleSpec? entryTextStyle, + }) { + // Set defaults if empty. + position ??= defaultBehaviorPosition; + outsideJustification ??= defaultOutsideJustification; + insideJustification ??= defaultInsideJustification; + cellPadding ??= defaultCellPadding; + + // Set the tabular layout settings to match the position if it is not + // specified. + horizontalFirst ??= (position == common.BehaviorPosition.top || + position == common.BehaviorPosition.bottom || + position == common.BehaviorPosition.inside); + final layoutBuilder = horizontalFirst + ? new TabularLegendLayout.horizontalFirst( + desiredMaxColumns: desiredMaxColumns, cellPadding: cellPadding) + : new TabularLegendLayout.verticalFirst( + desiredMaxRows: desiredMaxRows, cellPadding: cellPadding); + + return new DatumLegend._internal( + contentBuilder: + new TabularLegendContentBuilder(legendLayout: layoutBuilder), + selectionModelType: common.SelectionModelType.info, + position: position, + outsideJustification: outsideJustification, + insideJustification: insideJustification, + showMeasures: showMeasures ?? false, + legendDefaultMeasure: + legendDefaultMeasure ?? common.LegendDefaultMeasure.none, + measureFormatter: measureFormatter, + secondaryMeasureFormatter: secondaryMeasureFormatter, + entryTextStyle: entryTextStyle); + } + + /// Create a legend with custom layout. + /// + /// By default, the legend is place above the chart and horizontally aligned + /// to the start of the draw area. + /// + /// [contentBuilder] builder for the custom layout. + /// + /// [position] the legend will be positioned relative to the chart. Default + /// position is top. + /// + /// [outsideJustification] justification of the legend relative to the chart + /// if the position is top, bottom, left, right. Default to start of the draw + /// area. + /// + /// [insideJustification] justification of the legend relative to the chart if + /// the position is inside. Default to top of the chart, start of draw area. + /// Start of draw area means left for LTR directionality, and right for RTL. + /// + /// [showMeasures] show measure values for each series. + /// + /// [legendDefaultMeasure] if measure should show when there is no selection. + /// This is set to none by default (only shows measure for selected data). + /// + /// [measureFormatter] formats measure value if measures are shown. + /// + /// [secondaryMeasureFormatter] formats measures if measures are shown for the + /// series that uses secondary measure axis. + factory DatumLegend.customLayout( + LegendContentBuilder contentBuilder, { + common.BehaviorPosition? position, + common.OutsideJustification? outsideJustification, + common.InsideJustification? insideJustification, + bool? showMeasures, + common.LegendDefaultMeasure? legendDefaultMeasure, + common.MeasureFormatter? measureFormatter, + common.MeasureFormatter? secondaryMeasureFormatter, + common.TextStyleSpec? entryTextStyle, + }) { + // Set defaults if empty. + position ??= defaultBehaviorPosition; + outsideJustification ??= defaultOutsideJustification; + insideJustification ??= defaultInsideJustification; + + return new DatumLegend._internal( + contentBuilder: contentBuilder, + selectionModelType: common.SelectionModelType.info, + position: position, + outsideJustification: outsideJustification, + insideJustification: insideJustification, + showMeasures: showMeasures ?? false, + legendDefaultMeasure: + legendDefaultMeasure ?? common.LegendDefaultMeasure.none, + measureFormatter: measureFormatter, + secondaryMeasureFormatter: secondaryMeasureFormatter, + entryTextStyle: entryTextStyle, + ); + } + + DatumLegend._internal({ + required this.contentBuilder, + this.selectionModelType, + required this.position, + required this.outsideJustification, + required this.insideJustification, + required this.showMeasures, + this.legendDefaultMeasure, + this.measureFormatter, + this.secondaryMeasureFormatter, + this.entryTextStyle, + }); + + @override + common.DatumLegend createCommonBehavior() => + new _FlutterDatumLegend(this); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) { + (commonBehavior as _FlutterDatumLegend).config = this; + } + + /// All Legend behaviors get the same role ID, because you should only have + /// one legend on a chart. + @override + String get role => 'legend'; + + @override + bool operator ==(Object o) { + return o is DatumLegend && + selectionModelType == o.selectionModelType && + contentBuilder == o.contentBuilder && + position == o.position && + outsideJustification == o.outsideJustification && + insideJustification == o.insideJustification && + showMeasures == o.showMeasures && + legendDefaultMeasure == o.legendDefaultMeasure && + measureFormatter == o.measureFormatter && + secondaryMeasureFormatter == o.secondaryMeasureFormatter && + entryTextStyle == o.entryTextStyle; + } + + @override + int get hashCode { + return Object.hash( + selectionModelType, + contentBuilder, + position, + outsideJustification, + insideJustification, + showMeasures, + legendDefaultMeasure, + measureFormatter, + secondaryMeasureFormatter, + entryTextStyle); + } +} + +/// Flutter specific wrapper on the common Legend for building content. +class _FlutterDatumLegend extends common.DatumLegend + implements BuildableBehavior, TappableLegend { + DatumLegend config; + + _FlutterDatumLegend(this.config) + : super( + selectionModelType: config.selectionModelType, + measureFormatter: config.measureFormatter, + secondaryMeasureFormatter: config.secondaryMeasureFormatter, + legendDefaultMeasure: config.legendDefaultMeasure, + ) { + super.entryTextStyle = config.entryTextStyle; + } + + @override + void updateLegend() { + (chartContext as ChartContainerRenderObject).requestRebuild(); + } + + @override + common.BehaviorPosition get position => config.position; + + @override + common.OutsideJustification get outsideJustification => + config.outsideJustification; + + @override + common.InsideJustification get insideJustification => + config.insideJustification; + + @override + Widget build(BuildContext context) { + final hasSelection = + legendState.legendEntries.any((entry) => entry.isSelected); + + // Show measures if [showMeasures] is true and there is a selection or if + // showing measures when there is no selection. + final showMeasures = config.showMeasures && + (hasSelection || + legendDefaultMeasure != common.LegendDefaultMeasure.none); + + return config.contentBuilder + .build(context, legendState, this, showMeasures: showMeasures); + } + + /// TODO: Maybe highlight the pie wedge. + @override + onLegendEntryTapUp(common.LegendEntry detail) {} +} diff --git a/lib/src/behaviors/legend/legend.dart b/lib/src/behaviors/legend/legend.dart new file mode 100644 index 0000000..5cd347b --- /dev/null +++ b/lib/src/behaviors/legend/legend.dart @@ -0,0 +1,22 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' show LegendEntry, LegendTapHandling; + +abstract class TappableLegend { + /// Delegates handling of legend entry clicks according to the configured + /// [LegendTapHandling] strategy. + onLegendEntryTapUp(LegendEntry detail); +} diff --git a/lib/src/behaviors/legend/legend_content_builder.dart b/lib/src/behaviors/legend/legend_content_builder.dart new file mode 100644 index 0000000..4463788 --- /dev/null +++ b/lib/src/behaviors/legend/legend_content_builder.dart @@ -0,0 +1,92 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show Legend, LegendState, SeriesLegend; +import 'package:flutter/widgets.dart' show BuildContext, hashValues, Widget; +import 'legend.dart'; +import 'legend_entry_layout.dart'; +import 'legend_layout.dart'; + +/// Strategy for building a legend content widget. +abstract class LegendContentBuilder { + const LegendContentBuilder(); + + Widget build(BuildContext context, common.LegendState legendState, + common.Legend legend, + {bool showMeasures}); +} + +/// Base strategy for building a legend content widget. +/// +/// Each legend entry is passed to a [LegendLayout] strategy to create a widget +/// for each legend entry. These widgets are then passed to a +/// [LegendEntryLayout] strategy to create the legend widget. +abstract class BaseLegendContentBuilder implements LegendContentBuilder { + /// Strategy for creating one widget or each legend entry. + LegendEntryLayout get legendEntryLayout; + + /// Strategy for creating the legend content widget from a list of widgets. + /// + /// This is typically the list of widgets from legend entries. + LegendLayout get legendLayout; + + @override + Widget build(BuildContext context, common.LegendState legendState, + common.Legend legend, + {bool showMeasures = false}) { + final entryWidgets = legendState.legendEntries.map((entry) { + var isHidden = false; + if (legend is common.SeriesLegend) { + isHidden = legend.isSeriesHidden(entry.series.id); + } + + return legendEntryLayout.build( + context, entry, legend as TappableLegend, isHidden, + showMeasures: showMeasures); + }).toList(); + + return legendLayout.build(context, entryWidgets); + } +} + +// TODO: Expose settings for tabular layout. +/// Strategy that builds a tabular legend. +/// +/// [legendEntryLayout] custom strategy for creating widgets for each legend +/// entry. +/// [legendLayout] custom strategy for creating legend widget from list of +/// widgets that represent a legend entry. +class TabularLegendContentBuilder extends BaseLegendContentBuilder { + final LegendEntryLayout legendEntryLayout; + final LegendLayout legendLayout; + + TabularLegendContentBuilder( + {LegendEntryLayout? legendEntryLayout, LegendLayout? legendLayout}) + : this.legendEntryLayout = + legendEntryLayout ?? const SimpleLegendEntryLayout(), + this.legendLayout = + legendLayout ?? new TabularLegendLayout.horizontalFirst(); + + @override + bool operator ==(Object o) { + return o is TabularLegendContentBuilder && + legendEntryLayout == o.legendEntryLayout && + legendLayout == o.legendLayout; + } + + @override + int get hashCode => Object.hash(legendEntryLayout, legendLayout); +} diff --git a/lib/src/behaviors/legend/legend_entry_layout.dart b/lib/src/behaviors/legend/legend_entry_layout.dart new file mode 100644 index 0000000..fa38b84 --- /dev/null +++ b/lib/src/behaviors/legend/legend_entry_layout.dart @@ -0,0 +1,146 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common; +import 'package:charts_flutter/src/util/color.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart' + show GestureDetector, GestureTapUpCallback, TapUpDetails, Theme; + +import '../../symbol_renderer.dart'; +import 'legend.dart' show TappableLegend; + +/// Strategy for building one widget from one [common.LegendEntry]. +abstract class LegendEntryLayout { + Widget build(BuildContext context, common.LegendEntry legendEntry, + TappableLegend legend, bool isHidden, + {bool showMeasures}); +} + +/// Builds one legend entry as a row with symbol and label from the series. +/// +/// If directionality from the chart context indicates RTL, the symbol is placed +/// to the right of the text instead of the left of the text. +class SimpleLegendEntryLayout implements LegendEntryLayout { + const SimpleLegendEntryLayout(); + + Widget createSymbol(BuildContext context, common.LegendEntry legendEntry, + TappableLegend legend, bool isHidden) { + // TODO: Consider allowing scaling the size for the symbol. + // A custom symbol renderer can ignore this size and use their own. + final materialSymbolSize = new Size(12.0, 12.0); + + final entryColor = legendEntry.color; + final color = entryColor == null ? null : ColorUtil.toDartColor(entryColor); + + // Get the SymbolRendererBuilder wrapping a common.SymbolRenderer if needed. + final SymbolRendererBuilder symbolRendererBuilder = + legendEntry.symbolRenderer! is SymbolRendererBuilder + ? legendEntry.symbolRenderer! as SymbolRendererBuilder + : new SymbolRendererCanvas( + legendEntry.symbolRenderer!, legendEntry.dashPattern); + + return new GestureDetector( + child: symbolRendererBuilder.build( + context, + size: materialSymbolSize, + color: color, + enabled: !isHidden, + ), + onTapUp: makeTapUpCallback(context, legendEntry, legend)); + } + + Widget createLabel(BuildContext context, common.LegendEntry legendEntry, + TappableLegend legend, bool isHidden) { + TextStyle style = + _convertTextStyle(isHidden, context, legendEntry.textStyle); + + return new GestureDetector( + child: new Text(legendEntry.label, style: style), + onTapUp: makeTapUpCallback(context, legendEntry, legend)); + } + + Widget createMeasureValue(BuildContext context, + common.LegendEntry legendEntry, TappableLegend legend, bool isHidden) { + return new GestureDetector( + child: new Text(legendEntry.formattedValue!), + onTapUp: makeTapUpCallback(context, legendEntry, legend)); + } + + @override + Widget build(BuildContext context, common.LegendEntry legendEntry, + TappableLegend legend, bool isHidden, + {bool showMeasures = false}) { + final rowChildren = []; + + // TODO: Allow setting to configure the padding. + final padding = new EdgeInsets.only(right: 8.0); // Material default. + final symbol = createSymbol(context, legendEntry, legend, isHidden); + final label = createLabel(context, legendEntry, legend, isHidden); + + final measure = showMeasures + ? createMeasureValue(context, legendEntry, legend, isHidden) + : null; + + rowChildren.add(symbol); + rowChildren.add(new Container(padding: padding)); + rowChildren.add(label); + if (measure != null) { + rowChildren.add(new Container(padding: padding)); + rowChildren.add(measure); + } + + // Row automatically reverses the content if Directionality is rtl. + return new Row(children: rowChildren); + } + + GestureTapUpCallback makeTapUpCallback(BuildContext context, + common.LegendEntry legendEntry, TappableLegend legend) { + return (TapUpDetails d) { + legend.onLegendEntryTapUp(legendEntry); + }; + } + + bool operator ==(Object other) => other is SimpleLegendEntryLayout; + + int get hashCode { + return this.runtimeType.hashCode; + } + + /// Convert the charts common TextStlyeSpec into a standard TextStyle, while + /// reducing the color opacity to 26% if the entry is hidden. + /// + /// For non-specified values, override the hidden text color to use the body 1 + /// theme, but allow other properties of [Text] to be inherited. + TextStyle _convertTextStyle( + bool isHidden, BuildContext context, common.TextStyleSpec? textStyle) { + Color? color = textStyle?.color != null + ? ColorUtil.toDartColor(textStyle!.color!) + : null; + if (isHidden) { + // Use a default color for hidden legend entries if none is provided. + color ??= Theme.of(context).textTheme.bodyMedium!.color; + color = color!.withOpacity(0.26); + } + + return new TextStyle( + inherit: true, + fontFamily: textStyle?.fontFamily, + fontSize: textStyle?.fontSize != null + ? textStyle!.fontSize!.toDouble() + : null, + color: color); + } +} diff --git a/lib/src/behaviors/legend/legend_layout.dart b/lib/src/behaviors/legend/legend_layout.dart new file mode 100644 index 0000000..b8e86ec --- /dev/null +++ b/lib/src/behaviors/legend/legend_layout.dart @@ -0,0 +1,162 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show min; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +/// Strategy for building legend from legend entry widgets. +abstract class LegendLayout { + Widget build(BuildContext context, List legendEntryWidgets); +} + +/// Layout legend entries in tabular format. +class TabularLegendLayout implements LegendLayout { + /// No limit for max rows or max columns. + static const _noLimit = -1; + + /// Default EdgeInsets for padding rows to the max column count + static const defaultCellPadding = const EdgeInsets.all(8.0); + + final bool isHorizontalFirst; + final int desiredMaxRows; + final int desiredMaxColumns; + final EdgeInsets? cellPadding; + + TabularLegendLayout._internal( + {required this.isHorizontalFirst, + required this.desiredMaxRows, + required this.desiredMaxColumns, + this.cellPadding}); + + /// Layout horizontally until columns exceed [desiredMaxColumns]. + /// + /// [desiredMaxColumns] the max columns to use before laying out items in a + /// new row. By default there is no limit. The max columns created is the + /// smaller of desiredMaxColumns and number of legend entries. + /// + /// [cellPadding] the [EdgeInsets] for each widget. + factory TabularLegendLayout.horizontalFirst({ + int? desiredMaxColumns, + EdgeInsets? cellPadding, + }) { + return new TabularLegendLayout._internal( + isHorizontalFirst: true, + desiredMaxRows: _noLimit, + desiredMaxColumns: desiredMaxColumns ?? _noLimit, + cellPadding: cellPadding, + ); + } + + /// Layout vertically, until rows exceed [desiredMaxRows]. + /// + /// [desiredMaxRows] the max rows to use before layout out items in a new + /// column. By default there is no limit. The max columns created is the + /// smaller of desiredMaxRows and number of legend entries. + /// + /// [cellPadding] the [EdgeInsets] for each widget. + factory TabularLegendLayout.verticalFirst({ + int? desiredMaxRows, + EdgeInsets? cellPadding, + }) { + return new TabularLegendLayout._internal( + isHorizontalFirst: false, + desiredMaxRows: desiredMaxRows ?? _noLimit, + desiredMaxColumns: _noLimit, + cellPadding: cellPadding, + ); + } + + @override + Widget build(BuildContext context, List legendEntries) { + final paddedLegendEntries = ((cellPadding == null) + ? legendEntries + : legendEntries + .map((entry) => new Padding(padding: cellPadding!, child: entry)) + .toList()); + + return isHorizontalFirst + ? _buildHorizontalFirst(paddedLegendEntries) + : _buildVerticalFirst(paddedLegendEntries); + } + + @override + bool operator ==(o) => + o is TabularLegendLayout && + desiredMaxRows == o.desiredMaxRows && + desiredMaxColumns == o.desiredMaxColumns && + isHorizontalFirst == o.isHorizontalFirst && + cellPadding == o.cellPadding; + + @override + int get hashCode => Object.hash( + desiredMaxRows, desiredMaxColumns, isHorizontalFirst, cellPadding); + + Widget _buildHorizontalFirst(List legendEntries) { + final maxColumns = (desiredMaxColumns == _noLimit) + ? legendEntries.length + : min(legendEntries.length, desiredMaxColumns); + + final rows = []; + for (var i = 0; i < legendEntries.length; i += maxColumns) { + rows.add(new TableRow( + children: legendEntries + .sublist(i, min(i + maxColumns, legendEntries.length)) + .toList())); + } + + return _buildTableFromRows(rows); + } + + Widget _buildVerticalFirst(List legendEntries) { + final maxRows = (desiredMaxRows == _noLimit) + ? legendEntries.length + : min(legendEntries.length, desiredMaxRows); + + final rows = + new List.generate(maxRows, (_) => new TableRow(children: [])); + for (var i = 0; i < legendEntries.length; i++) { + rows[i % maxRows].children!.add(legendEntries[i]); + } + + return _buildTableFromRows(rows); + } + + Table _buildTableFromRows(List rows) { + final padWidget = Padding(padding: cellPadding ?? defaultCellPadding); + + // Pad rows to the max column count, because each TableRow in a table is + // required to have the same number of children. + final columnCount = rows + .map((r) => r.children!.length) + .fold(0, (max, current) => (current > max) ? current : max); + + for (var i = 0; i < rows.length; i++) { + final rowChildren = rows[i].children; + final padCount = columnCount - rowChildren!.length; + if (padCount > 0) { + rowChildren + .addAll(new Iterable.generate(padCount, (_) => padWidget)); + } + } + + // TODO: Investigate other means of creating the tabular legend + // Sizing the column width using [IntrinsicColumnWidth] is expensive per + // Flutter's documentation, but has to be used if the table is desired to + // have a width that is tight on each column. + return new Table( + children: rows, defaultColumnWidth: new IntrinsicColumnWidth()); + } +} diff --git a/lib/src/behaviors/legend/series_legend.dart b/lib/src/behaviors/legend/series_legend.dart new file mode 100644 index 0000000..652d45d --- /dev/null +++ b/lib/src/behaviors/legend/series_legend.dart @@ -0,0 +1,383 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show + BehaviorPosition, + ChartBehavior, + InsideJustification, + LegendEntry, + LegendTapHandling, + MeasureFormatter, + LegendDefaultMeasure, + OutsideJustification, + SeriesLegend, + SelectionModelType, + TextStyleSpec; +import 'package:collection/collection.dart' show ListEquality; +import 'package:flutter/widgets.dart' + show BuildContext, EdgeInsets, Widget, hashValues; +import 'package:meta/meta.dart' show immutable; +import '../../chart_container.dart' show ChartContainerRenderObject; +import '../chart_behavior.dart' + show BuildableBehavior, ChartBehavior, GestureType; +import 'legend.dart' show TappableLegend; +import 'legend_content_builder.dart' + show LegendContentBuilder, TabularLegendContentBuilder; +import 'legend_layout.dart' show TabularLegendLayout; + +/// Series legend behavior for charts. +@immutable +class SeriesLegend extends ChartBehavior { + static const defaultBehaviorPosition = common.BehaviorPosition.top; + static const defaultOutsideJustification = + common.OutsideJustification.startDrawArea; + static const defaultInsideJustification = common.InsideJustification.topStart; + + final desiredGestures = new Set(); + + final common.SelectionModelType? selectionModelType; + + /// Builder for creating custom legend content. + final LegendContentBuilder contentBuilder; + + /// Position of the legend relative to the chart. + final common.BehaviorPosition position; + + /// Justification of the legend relative to the chart + final common.OutsideJustification outsideJustification; + final common.InsideJustification insideJustification; + + /// Whether or not the legend should show measures. + /// + /// By default this is false, measures are not shown. When set to true, the + /// default behavior is to show measure only if there is selected data. + /// Please set [legendDefaultMeasure] to something other than none to enable + /// showing measures when there is no selection. + /// + /// This flag is used by the [contentBuilder], so a custom content builder + /// has to choose if it wants to use this flag. + final bool showMeasures; + + /// Option to show measures when selection is null. + /// + /// By default this is set to none, so no measures are shown when there is + /// no selection. + final common.LegendDefaultMeasure? legendDefaultMeasure; + + /// Formatter for measure value(s) if the measures are shown on the legend. + final common.MeasureFormatter? measureFormatter; + + /// Formatter for secondary measure value(s) if the measures are shown on the + /// legend and the series uses the secondary axis. + final common.MeasureFormatter? secondaryMeasureFormatter; + + /// Styles for legend entry label text. + final common.TextStyleSpec? entryTextStyle; + + static const defaultCellPadding = const EdgeInsets.all(8.0); + + final List? defaultHiddenSeries; + + /// Create a new tabular layout legend. + /// + /// By default, the legend is place above the chart and horizontally aligned + /// to the start of the draw area. + /// + /// [position] the legend will be positioned relative to the chart. Default + /// position is top. + /// + /// [outsideJustification] justification of the legend relative to the chart + /// if the position is top, bottom, left, right. Default to start of the draw + /// area. + /// + /// [insideJustification] justification of the legend relative to the chart if + /// the position is inside. Default to top of the chart, start of draw area. + /// Start of draw area means left for LTR directionality, and right for RTL. + /// + /// [horizontalFirst] if true, legend entries will grow horizontally first + /// instead of vertically first. If the position is top, bottom, or inside, + /// this defaults to true. Otherwise false. + /// + /// [desiredMaxRows] the max rows to use before layout out items in a new + /// column. By default there is no limit. The max columns created is the + /// smaller of desiredMaxRows and number of legend entries. + /// + /// [desiredMaxColumns] the max columns to use before laying out items in a + /// new row. By default there is no limit. The max columns created is the + /// smaller of desiredMaxColumns and number of legend entries. + /// + /// [defaultHiddenSeries] lists the IDs of series that should be hidden on + /// first chart draw. + /// + /// [showMeasures] show measure values for each series. + /// + /// [legendDefaultMeasure] if measure should show when there is no selection. + /// This is set to none by default (only shows measure for selected data). + /// + /// [measureFormatter] formats measure value if measures are shown. + /// + /// [secondaryMeasureFormatter] formats measures if measures are shown for the + /// series that uses secondary measure axis. + factory SeriesLegend({ + common.BehaviorPosition? position, + common.OutsideJustification? outsideJustification, + common.InsideJustification? insideJustification, + bool? horizontalFirst, + int? desiredMaxRows, + int? desiredMaxColumns, + EdgeInsets? cellPadding, + List? defaultHiddenSeries, + bool? showMeasures, + common.LegendDefaultMeasure? legendDefaultMeasure, + common.MeasureFormatter? measureFormatter, + common.MeasureFormatter? secondaryMeasureFormatter, + common.TextStyleSpec? entryTextStyle, + }) { + // Set defaults if empty. + position ??= defaultBehaviorPosition; + outsideJustification ??= defaultOutsideJustification; + insideJustification ??= defaultInsideJustification; + cellPadding ??= defaultCellPadding; + + // Set the tabular layout settings to match the position if it is not + // specified. + horizontalFirst ??= (position == common.BehaviorPosition.top || + position == common.BehaviorPosition.bottom || + position == common.BehaviorPosition.inside); + final layoutBuilder = horizontalFirst + ? new TabularLegendLayout.horizontalFirst( + desiredMaxColumns: desiredMaxColumns, cellPadding: cellPadding) + : new TabularLegendLayout.verticalFirst( + desiredMaxRows: desiredMaxRows, cellPadding: cellPadding); + + return new SeriesLegend._internal( + contentBuilder: + new TabularLegendContentBuilder(legendLayout: layoutBuilder), + selectionModelType: common.SelectionModelType.info, + position: position, + outsideJustification: outsideJustification, + insideJustification: insideJustification, + defaultHiddenSeries: defaultHiddenSeries, + showMeasures: showMeasures ?? false, + legendDefaultMeasure: + legendDefaultMeasure ?? common.LegendDefaultMeasure.none, + measureFormatter: measureFormatter, + secondaryMeasureFormatter: secondaryMeasureFormatter, + entryTextStyle: entryTextStyle); + } + + /// Create a legend with custom layout. + /// + /// By default, the legend is place above the chart and horizontally aligned + /// to the start of the draw area. + /// + /// [contentBuilder] builder for the custom layout. + /// + /// [position] the legend will be positioned relative to the chart. Default + /// position is top. + /// + /// [outsideJustification] justification of the legend relative to the chart + /// if the position is top, bottom, left, right. Default to start of the draw + /// area. + /// + /// [insideJustification] justification of the legend relative to the chart if + /// the position is inside. Default to top of the chart, start of draw area. + /// Start of draw area means left for LTR directionality, and right for RTL. + /// + /// [defaultHiddenSeries] lists the IDs of series that should be hidden on + /// first chart draw. + /// + /// [showMeasures] show measure values for each series. + /// + /// [legendDefaultMeasure] if measure should show when there is no selection. + /// This is set to none by default (only shows measure for selected data). + /// + /// [measureFormatter] formats measure value if measures are shown. + /// + /// [secondaryMeasureFormatter] formats measures if measures are shown for the + /// series that uses secondary measure axis. + factory SeriesLegend.customLayout( + LegendContentBuilder contentBuilder, { + common.BehaviorPosition? position, + common.OutsideJustification? outsideJustification, + common.InsideJustification? insideJustification, + List? defaultHiddenSeries, + bool? showMeasures, + common.LegendDefaultMeasure? legendDefaultMeasure, + common.MeasureFormatter? measureFormatter, + common.MeasureFormatter? secondaryMeasureFormatter, + common.TextStyleSpec? entryTextStyle, + }) { + // Set defaults if empty. + position ??= defaultBehaviorPosition; + outsideJustification ??= defaultOutsideJustification; + insideJustification ??= defaultInsideJustification; + + return new SeriesLegend._internal( + contentBuilder: contentBuilder, + selectionModelType: common.SelectionModelType.info, + position: position, + outsideJustification: outsideJustification, + insideJustification: insideJustification, + defaultHiddenSeries: defaultHiddenSeries, + showMeasures: showMeasures ?? false, + legendDefaultMeasure: + legendDefaultMeasure ?? common.LegendDefaultMeasure.none, + measureFormatter: measureFormatter, + secondaryMeasureFormatter: secondaryMeasureFormatter, + entryTextStyle: entryTextStyle, + ); + } + + SeriesLegend._internal({ + required this.contentBuilder, + this.selectionModelType, + required this.position, + required this.outsideJustification, + required this.insideJustification, + this.defaultHiddenSeries, + required this.showMeasures, + this.legendDefaultMeasure, + this.measureFormatter, + this.secondaryMeasureFormatter, + this.entryTextStyle, + }); + + @override + common.SeriesLegend createCommonBehavior() => + new _FlutterSeriesLegend(this); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) { + (commonBehavior as _FlutterSeriesLegend).config = this; + } + + /// All Legend behaviors get the same role ID, because you should only have + /// one legend on a chart. + @override + String get role => 'legend'; + + @override + bool operator ==(Object o) { + return o is SeriesLegend && + selectionModelType == o.selectionModelType && + contentBuilder == o.contentBuilder && + position == o.position && + outsideJustification == o.outsideJustification && + insideJustification == o.insideJustification && + new ListEquality().equals(defaultHiddenSeries, o.defaultHiddenSeries) && + showMeasures == o.showMeasures && + legendDefaultMeasure == o.legendDefaultMeasure && + measureFormatter == o.measureFormatter && + secondaryMeasureFormatter == o.secondaryMeasureFormatter && + entryTextStyle == o.entryTextStyle; + } + + @override + int get hashCode { + return Object.hash( + selectionModelType, + contentBuilder, + position, + outsideJustification, + insideJustification, + defaultHiddenSeries, + showMeasures, + legendDefaultMeasure, + measureFormatter, + secondaryMeasureFormatter, + entryTextStyle); + } +} + +/// Flutter specific wrapper on the common Legend for building content. +class _FlutterSeriesLegend extends common.SeriesLegend + implements BuildableBehavior, TappableLegend { + SeriesLegend config; + + _FlutterSeriesLegend(this.config) + : super( + selectionModelType: config.selectionModelType, + measureFormatter: config.measureFormatter, + secondaryMeasureFormatter: config.secondaryMeasureFormatter, + legendDefaultMeasure: config.legendDefaultMeasure, + ) { + super.defaultHiddenSeries = config.defaultHiddenSeries; + super.entryTextStyle = config.entryTextStyle; + } + + @override + void updateLegend() { + (chartContext as ChartContainerRenderObject).requestRebuild(); + } + + @override + common.BehaviorPosition get position => config.position; + + @override + common.OutsideJustification get outsideJustification => + config.outsideJustification; + + @override + common.InsideJustification get insideJustification => + config.insideJustification; + + @override + Widget build(BuildContext context) { + final hasSelection = legendState.legendEntries != null && + legendState.legendEntries.any((entry) => entry.isSelected); + + // Show measures if [showMeasures] is true and there is a selection or if + // showing measures when there is no selection. + final showMeasures = config.showMeasures && + (hasSelection || + legendDefaultMeasure != common.LegendDefaultMeasure.none); + + return config.contentBuilder + .build(context, legendState, this, showMeasures: showMeasures); + } + + @override + onLegendEntryTapUp(common.LegendEntry detail) { + switch (legendTapHandling) { + case common.LegendTapHandling.hide: + _hideSeries(detail); + break; + + case common.LegendTapHandling.none: + default: + break; + } + } + + /// Handles tap events by hiding or un-hiding entries tapped in the legend. + /// + /// Tapping on a visible series in the legend will hide it. Tapping on a + /// hidden series will make it visible again. + void _hideSeries(common.LegendEntry detail) { + final seriesId = detail.series.id; + + // Handle the event by toggling the hidden state of the target. + if (isSeriesHidden(seriesId)) { + showSeries(seriesId); + } else { + hideSeries(seriesId); + } + + // Redraw the chart to actually hide hidden series. + chart.redraw(skipLayout: true, skipAnimation: false); + } +} diff --git a/lib/src/behaviors/line_point_highlighter.dart b/lib/src/behaviors/line_point_highlighter.dart new file mode 100644 index 0000000..957a8bd --- /dev/null +++ b/lib/src/behaviors/line_point_highlighter.dart @@ -0,0 +1,128 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:collection/collection.dart' show ListEquality; +import 'package:charts_common/common.dart' as common + show + ChartBehavior, + LinePointHighlighter, + LinePointHighlighterFollowLineType, + SelectionModelType, + SymbolRenderer; +import 'package:flutter/widgets.dart' ; +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that monitors the specified [SelectionModel] and darkens the +/// color for selected data. +/// +/// This is typically used for bars and pies to highlight segments. +/// +/// It is used in combination with SelectNearest to update the selection model +/// and expand selection out to the domain value. +@immutable +class LinePointHighlighter extends ChartBehavior { + final desiredGestures = new Set(); + + final common.SelectionModelType? selectionModelType; + + /// Default radius of the dots if the series has no radius mapping function. + /// + /// When no radius mapping function is provided, this value will be used as + /// is. [radiusPaddingPx] will not be added to [defaultRadiusPx]. + final double? defaultRadiusPx; + + /// Additional radius value added to the radius of the selected data. + /// + /// This value is only used when the series has a radius mapping function + /// defined. + final double? radiusPaddingPx; + + final common.LinePointHighlighterFollowLineType? showHorizontalFollowLine; + + final common.LinePointHighlighterFollowLineType? showVerticalFollowLine; + + /// The dash pattern to be used for drawing the line. + /// + /// To disable dash pattern (to draw a solid line), pass in an empty list. + /// This is because if dashPattern is null or not set, it defaults to [1,3]. + final List? dashPattern; + + /// Whether or not follow lines should be drawn across the entire chart draw + /// area, or just from the axis to the point. + /// + /// When disabled, measure follow lines will be drawn from the primary measure + /// axis to the point. In RTL mode, this means from the right-hand axis. In + /// LTR mode, from the left-hand axis. + final bool? drawFollowLinesAcrossChart; + + /// Renderer used to draw the highlighted points. + final common.SymbolRenderer? symbolRenderer; + + LinePointHighlighter( + {this.selectionModelType, + this.defaultRadiusPx, + this.radiusPaddingPx, + this.showHorizontalFollowLine, + this.showVerticalFollowLine, + this.dashPattern, + this.drawFollowLinesAcrossChart, + this.symbolRenderer}); + + @override + common.LinePointHighlighter createCommonBehavior() => + new common.LinePointHighlighter( + selectionModelType: selectionModelType, + defaultRadiusPx: defaultRadiusPx, + radiusPaddingPx: radiusPaddingPx, + showHorizontalFollowLine: showHorizontalFollowLine, + showVerticalFollowLine: showVerticalFollowLine, + dashPattern: dashPattern, + drawFollowLinesAcrossChart: drawFollowLinesAcrossChart, + symbolRenderer: symbolRenderer, + ); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'LinePointHighlighter-${selectionModelType.toString()}'; + + @override + bool operator ==(Object o) { + return o is LinePointHighlighter && + defaultRadiusPx == o.defaultRadiusPx && + radiusPaddingPx == o.radiusPaddingPx && + showHorizontalFollowLine == o.showHorizontalFollowLine && + showVerticalFollowLine == o.showVerticalFollowLine && + selectionModelType == o.selectionModelType && + new ListEquality().equals(dashPattern, o.dashPattern) && + drawFollowLinesAcrossChart == o.drawFollowLinesAcrossChart; + } + + @override + int get hashCode { + return Object.hash( + selectionModelType, + defaultRadiusPx, + radiusPaddingPx, + showHorizontalFollowLine, + showVerticalFollowLine, + dashPattern, + drawFollowLinesAcrossChart, + ); + } +} diff --git a/lib/src/behaviors/range_annotation.dart b/lib/src/behaviors/range_annotation.dart new file mode 100644 index 0000000..8d1e07f --- /dev/null +++ b/lib/src/behaviors/range_annotation.dart @@ -0,0 +1,128 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show + AnnotationLabelAnchor, + AnnotationLabelDirection, + AnnotationLabelPosition, + AnnotationSegment, + ChartBehavior, + Color, + MaterialPalette, + RangeAnnotation, + TextStyleSpec; +import 'package:collection/collection.dart' show ListEquality; +import 'package:flutter/widgets.dart' ; +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that annotations domain ranges with a solid fill color. +/// +/// The annotations will be drawn underneath series data and chart axes. +/// +/// This is typically used for line charts to call out sections of the data +/// range. +@immutable +class RangeAnnotation extends ChartBehavior { + final desiredGestures = new Set(); + + /// List of annotations to render on the chart. + final List> annotations; + + /// Configures where to anchor annotation label text. + final common.AnnotationLabelAnchor? defaultLabelAnchor; + + /// Direction of label text on the annotations. + final common.AnnotationLabelDirection? defaultLabelDirection; + + /// Configures where to place labels relative to the annotation. + final common.AnnotationLabelPosition? defaultLabelPosition; + + /// Configures the style of label text. + final common.TextStyleSpec? defaultLabelStyleSpec; + + /// Default color for annotations. + final common.Color? defaultColor; + + /// Whether or not the range of the axis should be extended to include the + /// annotation start and end values. + final bool? extendAxis; + + /// Space before and after label text. + final int? labelPadding; + + /// Configures the order in which the behavior should be painted. + /// This value should be relative to LayoutPaintViewOrder.rangeAnnotation. + /// (e.g. LayoutViewPaintOrder.rangeAnnotation + 1) + final int? layoutPaintOrder; + + RangeAnnotation(this.annotations, + {common.Color? defaultColor, + this.defaultLabelAnchor, + this.defaultLabelDirection, + this.defaultLabelPosition, + this.defaultLabelStyleSpec, + this.extendAxis, + this.labelPadding, + this.layoutPaintOrder}) + : this.defaultColor = + defaultColor ?? common.MaterialPalette.gray.shade100; + + @override + common.RangeAnnotation createCommonBehavior() => + new common.RangeAnnotation(annotations, + defaultColor: defaultColor, + defaultLabelAnchor: defaultLabelAnchor, + defaultLabelDirection: defaultLabelDirection, + defaultLabelPosition: defaultLabelPosition, + defaultLabelStyleSpec: defaultLabelStyleSpec, + extendAxis: extendAxis, + labelPadding: labelPadding, + layoutPaintOrder: layoutPaintOrder); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'RangeAnnotation'; + + @override + bool operator ==(Object o) { + return o is RangeAnnotation && + new ListEquality().equals(annotations, o.annotations) && + defaultColor == o.defaultColor && + extendAxis == o.extendAxis && + defaultLabelAnchor == o.defaultLabelAnchor && + defaultLabelDirection == o.defaultLabelDirection && + defaultLabelPosition == o.defaultLabelPosition && + defaultLabelStyleSpec == o.defaultLabelStyleSpec && + labelPadding == o.labelPadding && + layoutPaintOrder == o.layoutPaintOrder; + } + + @override + int get hashCode => Object.hash( + annotations, + defaultColor, + extendAxis, + defaultLabelAnchor, + defaultLabelDirection, + defaultLabelPosition, + defaultLabelStyleSpec, + labelPadding, + layoutPaintOrder); +} diff --git a/lib/src/behaviors/select_nearest.dart b/lib/src/behaviors/select_nearest.dart new file mode 100644 index 0000000..7658367 --- /dev/null +++ b/lib/src/behaviors/select_nearest.dart @@ -0,0 +1,154 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show + ChartBehavior, + SelectNearest, + SelectionMode, + SelectionModelType, + SelectionTrigger; + +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that listens to the given eventTrigger and updates the +/// specified [SelectionModel]. This is used to pair input events to behaviors +/// that listen to selection changes. +/// +/// Input event types: +/// hover (default) - Mouse over/near data. +/// tap - Mouse/Touch on/near data. +/// pressHold - Mouse/Touch and drag across the data instead of panning. +/// longPressHold - Mouse/Touch for a while in one place then drag across the data. +/// +/// SelectionModels that can be updated: +/// info - To view the details of the selected items (ie: hover for web). +/// action - To select an item as an input, drill, or other selection. +/// +/// Other options available +/// selectionMode - Optional mode for expanding the selection beyond the +/// nearest datum. Defaults to expandToDomain. +/// selectClosestSeries - mark the series for the closest data point as +/// selected. (Default: true) +/// +/// You can add one SelectNearest for each model type that you are updating. +/// Any previous SelectNearest behavior for that selection model will be +/// removed. +@immutable +class SelectNearest extends ChartBehavior { + final Set desiredGestures; + + final common.SelectionModelType selectionModelType; + final common.SelectionTrigger eventTrigger; + final common.SelectionMode selectionMode; + final bool selectAcrossAllDrawAreaComponents; + final bool selectClosestSeries; + final int? maximumDomainDistancePx; + + SelectNearest._internal( + {required this.selectionModelType, + this.selectionMode = common.SelectionMode.expandToDomain, + this.selectAcrossAllDrawAreaComponents = false, + this.selectClosestSeries = true, + required this.eventTrigger, + required this.desiredGestures, + this.maximumDomainDistancePx}); + + factory SelectNearest( + {common.SelectionModelType selectionModelType = + common.SelectionModelType.info, + common.SelectionMode selectionMode = common.SelectionMode.expandToDomain, + bool selectAcrossAllDrawAreaComponents = false, + bool selectClosestSeries = true, + common.SelectionTrigger eventTrigger = common.SelectionTrigger.tap, + int? maximumDomainDistancePx}) { + return new SelectNearest._internal( + selectionModelType: selectionModelType, + selectionMode: selectionMode, + selectAcrossAllDrawAreaComponents: selectAcrossAllDrawAreaComponents, + selectClosestSeries: selectClosestSeries, + eventTrigger: eventTrigger, + desiredGestures: SelectNearest._getDesiredGestures(eventTrigger), + maximumDomainDistancePx: maximumDomainDistancePx); + } + + static Set _getDesiredGestures( + common.SelectionTrigger eventTrigger) { + final desiredGestures = new Set(); + switch (eventTrigger) { + case common.SelectionTrigger.tap: + desiredGestures..add(GestureType.onTap); + break; + case common.SelectionTrigger.tapAndDrag: + desiredGestures + ..add(GestureType.onTap) + ..add(GestureType.onDrag); + break; + case common.SelectionTrigger.pressHold: + case common.SelectionTrigger.longPressHold: + desiredGestures + ..add(GestureType.onTap) + ..add(GestureType.onLongPress) + ..add(GestureType.onDrag); + break; + case common.SelectionTrigger.hover: + default: + desiredGestures..add(GestureType.onHover); + break; + } + return desiredGestures; + } + + @override + common.SelectNearest createCommonBehavior() { + return new common.SelectNearest( + selectionModelType: selectionModelType, + eventTrigger: eventTrigger, + selectionMode: selectionMode, + selectClosestSeries: selectClosestSeries, + maximumDomainDistancePx: maximumDomainDistancePx); + } + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + // TODO: Explore the performance impact of calculating this once + // at the constructor for this and common ChartBehaviors. + @override + String get role => 'SelectNearest-${selectionModelType.toString()}}'; + + bool operator ==(Object other) { + if (other is SelectNearest) { + return (selectionModelType == other.selectionModelType) && + (eventTrigger == other.eventTrigger) && + (selectionMode == other.selectionMode) && + (selectClosestSeries == other.selectClosestSeries) && + (maximumDomainDistancePx == other.maximumDomainDistancePx); + } else { + return false; + } + } + + int get hashCode { + int hashcode = selectionModelType.hashCode; + hashcode = hashcode * 37 + eventTrigger.hashCode; + hashcode = hashcode * 37 + selectionMode.hashCode; + hashcode = hashcode * 37 + selectClosestSeries.hashCode; + hashcode = hashcode * 37 + maximumDomainDistancePx.hashCode; + return hashcode; + } +} diff --git a/lib/src/behaviors/slider/slider.dart b/lib/src/behaviors/slider/slider.dart new file mode 100644 index 0000000..6b69b88 --- /dev/null +++ b/lib/src/behaviors/slider/slider.dart @@ -0,0 +1,198 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Rectangle; +import 'package:charts_common/common.dart' as common + show + ChartBehavior, + LayoutViewPaintOrder, + RectSymbolRenderer, + SelectionTrigger, + Slider, + SliderListenerCallback, + SliderStyle, + SymbolRenderer; +import 'package:flutter/widgets.dart' ; +import 'package:meta/meta.dart' show immutable; + +import '../chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that adds a slider widget to a chart. When the slider is +/// dropped after drag, it will report its domain position and nearest datum +/// value. This behavior only supports charts that use continuous scales. +/// +/// Input event types: +/// tapAndDrag - Mouse/Touch on the handle and drag across the chart. +/// pressHold - Mouse/Touch on the handle and drag across the chart instead of +/// panning. +/// longPressHold - Mouse/Touch for a while on the handle, then drag across +/// the data. +@immutable +class Slider extends ChartBehavior { + final Set desiredGestures; + + /// Type of input event for the slider. + /// + /// Input event types: + /// tapAndDrag - Mouse/Touch on the handle and drag across the chart. + /// pressHold - Mouse/Touch on the handle and drag across the chart instead + /// of panning. + /// longPressHold - Mouse/Touch for a while on the handle, then drag across + /// the data. + final common.SelectionTrigger eventTrigger; + + /// The order to paint slider on the canvas. + /// + /// The smaller number is drawn first. This value should be relative to + /// LayoutPaintViewOrder.slider (e.g. LayoutViewPaintOrder.slider + 1). + final int? layoutPaintOrder; + + /// Initial domain position of the slider, in domain units. + final dynamic? initialDomainValue; + + /// Callback function that will be called when the position of the slider + /// changes during a drag event. + /// + /// The callback will be given the current domain position of the slider. + final common.SliderListenerCallback? onChangeCallback; + + /// Custom role ID for this slider + final String? roleId; + + /// Whether or not the slider will snap onto the nearest datum (by domain + /// distance) when dragged. + final bool snapToDatum; + + /// Color and size styles for the slider. + final common.SliderStyle? style; + + /// Renderer for the handle. Defaults to a rectangle. + final common.SymbolRenderer? handleRenderer; + + Slider._internal( + {required this.eventTrigger, + this.onChangeCallback, + this.initialDomainValue, + this.roleId, + required this.snapToDatum, + this.style, + this.handleRenderer, + required this.desiredGestures, + this.layoutPaintOrder}); + + /// Constructs a [Slider]. + /// + /// [eventTrigger] sets the type of gesture handled by the slider. + /// + /// [handleRenderer] draws a handle for the slider. Defaults to a rectangle. + /// + /// [initialDomainValue] sets the initial position of the slider in domain + /// units. The default is the center of the chart. + /// + /// [onChangeCallback] will be called when the position of the slider + /// changes during a drag event. + /// + /// [snapToDatum] configures the slider to snap snap onto the nearest datum + /// (by domain distance) when dragged. By default, the slider can be + /// positioned anywhere along the domain axis. + /// + /// [style] configures the color and sizing of the slider line and handle. + /// + /// [layoutPaintOrder] configures the order in which the behavior should be + /// painted. This value should be relative to LayoutPaintViewOrder.slider. + /// (e.g. LayoutViewPaintOrder.slider + 1). + factory Slider( + {common.SelectionTrigger? eventTrigger, + common.SymbolRenderer? handleRenderer, + dynamic? initialDomainValue, + String? roleId, + common.SliderListenerCallback? onChangeCallback, + bool snapToDatum = false, + common.SliderStyle? style, + int layoutPaintOrder = common.LayoutViewPaintOrder.slider}) { + eventTrigger ??= common.SelectionTrigger.tapAndDrag; + handleRenderer ??= new common.RectSymbolRenderer(); + // Default the handle size large enough to tap on a mobile device. + style ??= new common.SliderStyle(handleSize: Rectangle(0, 0, 20, 30)); + return new Slider._internal( + eventTrigger: eventTrigger, + handleRenderer: handleRenderer, + initialDomainValue: initialDomainValue, + onChangeCallback: onChangeCallback, + roleId: roleId, + snapToDatum: snapToDatum, + style: style, + desiredGestures: Slider._getDesiredGestures(eventTrigger), + layoutPaintOrder: layoutPaintOrder); + } + + static Set _getDesiredGestures( + common.SelectionTrigger eventTrigger) { + final desiredGestures = new Set(); + switch (eventTrigger) { + case common.SelectionTrigger.tapAndDrag: + desiredGestures + ..add(GestureType.onTap) + ..add(GestureType.onDrag); + break; + case common.SelectionTrigger.pressHold: + case common.SelectionTrigger.longPressHold: + desiredGestures + ..add(GestureType.onTap) + ..add(GestureType.onLongPress) + ..add(GestureType.onDrag); + break; + default: + throw new ArgumentError( + 'Slider does not support the event trigger ' + '"${eventTrigger}"'); + } + return desiredGestures; + } + + @override + common.Slider createCommonBehavior() => new common.Slider( + eventTrigger: eventTrigger, + handleRenderer: handleRenderer, + initialDomainValue: initialDomainValue as D, + onChangeCallback: onChangeCallback, + roleId: roleId, + snapToDatum: snapToDatum, + style: style); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'Slider-${eventTrigger.toString()}'; + + @override + bool operator ==(Object o) { + return o is Slider && + eventTrigger == o.eventTrigger && + handleRenderer == o.handleRenderer && + initialDomainValue == o.initialDomainValue && + onChangeCallback == o.onChangeCallback && + roleId == o.roleId && + snapToDatum == o.snapToDatum && + style == o.style && + layoutPaintOrder == o.layoutPaintOrder; + } + + @override + int get hashCode { + return Object.hash(eventTrigger, handleRenderer, initialDomainValue, roleId, + snapToDatum, style, layoutPaintOrder); + } +} diff --git a/lib/src/behaviors/sliding_viewport.dart b/lib/src/behaviors/sliding_viewport.dart new file mode 100644 index 0000000..f839d31 --- /dev/null +++ b/lib/src/behaviors/sliding_viewport.dart @@ -0,0 +1,53 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show ChartBehavior, SelectionModelType, SlidingViewport; + +import 'package:meta/meta.dart' show immutable; + +import 'chart_behavior.dart' show ChartBehavior, GestureType; + +/// Chart behavior that centers the viewport on the selected domain. +/// +/// It is used in combination with SelectNearest to update the selection model +/// and notify this behavior to update the viewport on selection change. +/// +/// This behavior can only be used on [CartesianChart]. +@immutable +class SlidingViewport extends ChartBehavior { + final desiredGestures = new Set(); + + final common.SelectionModelType selectionModelType; + + SlidingViewport([this.selectionModelType = common.SelectionModelType.info]); + + @override + common.SlidingViewport createCommonBehavior() => + new common.SlidingViewport(selectionModelType); + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'slidingViewport-${selectionModelType.toString()}'; + + @override + bool operator ==(Object o) => + o is SlidingViewport && selectionModelType == o.selectionModelType; + + @override + int get hashCode => selectionModelType.hashCode; +} diff --git a/lib/src/behaviors/zoom/initial_hint_behavior.dart b/lib/src/behaviors/zoom/initial_hint_behavior.dart new file mode 100644 index 0000000..40f7b58 --- /dev/null +++ b/lib/src/behaviors/zoom/initial_hint_behavior.dart @@ -0,0 +1,131 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/widgets.dart' show AnimationController; + +import 'package:charts_common/common.dart' as common + show BaseChart, ChartBehavior, InitialHintBehavior; +import 'package:meta/meta.dart' show immutable; + +import '../../base_chart_state.dart' show BaseChartState; +import '../chart_behavior.dart' + show ChartBehavior, ChartStateBehavior, GestureType; + +@immutable +class InitialHintBehavior extends ChartBehavior { + final desiredGestures = new Set(); + + final Duration? hintDuration; + final double? maxHintTranslate; + final double? maxHintScaleFactor; + + InitialHintBehavior( + {this.hintDuration, this.maxHintTranslate, this.maxHintScaleFactor}); + + @override + common.InitialHintBehavior createCommonBehavior() { + final behavior = new FlutterInitialHintBehavior(); + + if (hintDuration != null) { + behavior.hintDuration = hintDuration!; + } + + if (maxHintTranslate != null) { + behavior.maxHintTranslate = maxHintTranslate!; + } + + if (maxHintScaleFactor != null) { + behavior.maxHintScaleFactor = maxHintScaleFactor!; + } + + return behavior; + } + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'InitialHint'; + + bool operator ==(Object other) { + return other is InitialHintBehavior && other.hintDuration == hintDuration; + } + + int get hashCode { + return hintDuration.hashCode; + } +} + +/// Adds a native animation controller required for [common.InitialHintBehavior] +/// to function. +class FlutterInitialHintBehavior extends common.InitialHintBehavior + implements ChartStateBehavior { + AnimationController? _hintAnimator; + + BaseChartState? _chartState; + + set chartState(BaseChartState chartState) { + assert(chartState != null); + + _chartState = chartState; + + _hintAnimator = chartState.getAnimationController(this); + _hintAnimator?.addListener(onHintTick); + } + + @override + void startHintAnimation() { + super.startHintAnimation(); + + _hintAnimator! + ..duration = hintDuration + ..forward(from: 0.0); + } + + @override + void stopHintAnimation() { + super.stopHintAnimation(); + + _hintAnimator?.stop(); + // Hint animation occurs only on the first draw. The hint animator is no + // longer needed after the hint animation stops and is removed. + _chartState!.disposeAnimationController(this); + _hintAnimator = null; + } + + @override + double get hintAnimationPercent => _hintAnimator!.value; + + bool _skippedFirstTick = true; + + @override + void onHintTick() { + // Skip the first tick on Flutter because the widget rebuild scheduled + // during onAnimation fails on an assert on render object in the framework. + if (_skippedFirstTick) { + _skippedFirstTick = false; + return; + } + + super.onHintTick(); + } + + @override + removeFrom(common.BaseChart chart) { + _chartState!.disposeAnimationController(this); + _hintAnimator = null; + super.removeFrom(chart); + } +} diff --git a/lib/src/behaviors/zoom/pan_and_zoom_behavior.dart b/lib/src/behaviors/zoom/pan_and_zoom_behavior.dart new file mode 100644 index 0000000..3fbfe54 --- /dev/null +++ b/lib/src/behaviors/zoom/pan_and_zoom_behavior.dart @@ -0,0 +1,64 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show ChartBehavior, PanAndZoomBehavior, PanningCompletedCallback; +import 'package:meta/meta.dart' show immutable; + +import '../chart_behavior.dart' show ChartBehavior, GestureType; +import 'pan_behavior.dart' show FlutterPanBehaviorMixin; + +@immutable +class PanAndZoomBehavior extends ChartBehavior { + final _desiredGestures = new Set.from([ + GestureType.onDrag, + ]); + + Set get desiredGestures => _desiredGestures; + + /// Optional callback that is called when pan / zoom is completed. + /// + /// When flinging this callback is called after the fling is completed. + /// This is because panning is only completed when the flinging stops. + final common.PanningCompletedCallback? panningCompletedCallback; + + PanAndZoomBehavior({this.panningCompletedCallback}); + + @override + common.PanAndZoomBehavior createCommonBehavior() { + return new FlutterPanAndZoomBehavior() + ..panningCompletedCallback = panningCompletedCallback; + } + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'PanAndZoom'; + + bool operator ==(Object other) { + return other is PanAndZoomBehavior && + other.panningCompletedCallback == panningCompletedCallback; + } + + int get hashCode { + return panningCompletedCallback.hashCode; + } +} + +/// Adds fling gesture support to [common.PanAndZoomBehavior], by way of +/// [FlutterPanBehaviorMixin]. +class FlutterPanAndZoomBehavior extends common.PanAndZoomBehavior + with FlutterPanBehaviorMixin {} diff --git a/lib/src/behaviors/zoom/pan_behavior.dart b/lib/src/behaviors/zoom/pan_behavior.dart new file mode 100644 index 0000000..3668e65 --- /dev/null +++ b/lib/src/behaviors/zoom/pan_behavior.dart @@ -0,0 +1,186 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show max, pow, Point; +import 'dart:ui' show lerpDouble; + +import 'package:flutter/widgets.dart' show AnimationController; + +import 'package:charts_common/common.dart' as common + show BaseChart, ChartBehavior, PanBehavior, PanningCompletedCallback; +import 'package:meta/meta.dart' show immutable; + +import '../../base_chart_state.dart' show BaseChartState; +import '../chart_behavior.dart' + show ChartBehavior, ChartStateBehavior, GestureType; + +@immutable +class PanBehavior extends ChartBehavior { + final _desiredGestures = new Set.from([ + GestureType.onDrag, + ]); + + /// Optional callback that is called when panning is completed. + /// + /// When flinging this callback is called after the fling is completed. + /// This is because panning is only completed when the flinging stops. + final common.PanningCompletedCallback? panningCompletedCallback; + + PanBehavior({this.panningCompletedCallback}); + + Set get desiredGestures => _desiredGestures; + + @override + common.PanBehavior createCommonBehavior() { + return new FlutterPanBehavior() + ..panningCompletedCallback = panningCompletedCallback; + } + + @override + void updateCommonBehavior(common.ChartBehavior commonBehavior) {} + + @override + String get role => 'Pan'; + + bool operator ==(Object other) { + return other is PanBehavior && + other.panningCompletedCallback == panningCompletedCallback; + } + + int get hashCode { + return panningCompletedCallback.hashCode; + } +} + +/// Class extending [common.PanBehavior] with fling gesture support. +class FlutterPanBehavior = common.PanBehavior + with FlutterPanBehaviorMixin; + +/// Mixin that adds fling gesture support to [common.PanBehavior] or subclasses +/// thereof. +mixin FlutterPanBehaviorMixin on common.PanBehavior + implements ChartStateBehavior { + late BaseChartState _chartState; + + set chartState(BaseChartState chartState) { + assert(chartState != null); + + _chartState = chartState; + _flingAnimator = chartState.getAnimationController(this); + _flingAnimator?.addListener(_onFlingTick); + } + + AnimationController? _flingAnimator; + + double _flingAnimationInitialTranslatePx = 0; + double _flingAnimationTargetTranslatePx = 0; + + bool _isFlinging = false; + + static const flingDistanceMultiplier = 0.15; + static const flingDeceleratorFactor = 1.0; + static const flingDurationMultiplier = 0.15; + static const minimumFlingVelocity = 300.0; + + @override + removeFrom(common.BaseChart chart) { + stopFlingAnimation(); + _chartState.disposeAnimationController(this); + _flingAnimator = null; + super.removeFrom(chart); + } + + @override + bool onTapTest(Point chartPoint) { + super.onTapTest(chartPoint); + + stopFlingAnimation(); + + return true; + } + + @override + bool onDragEnd( + Point localPosition, double scale, double pixelsPerSec) { + if (isPanning) { + // Ignore slow drag gestures to avoid jitter. + if (pixelsPerSec.abs() < minimumFlingVelocity) { + onPanEnd(); + return true; + } + + _startFling(pixelsPerSec); + } + + return super.onDragEnd(localPosition, scale, pixelsPerSec); + } + + /// Starts a 'fling' in the direction and speed given by [pixelsPerSec]. + void _startFling(double pixelsPerSec) { + final domainAxis = chart!.domainAxis; + + _flingAnimationInitialTranslatePx = domainAxis!.viewportTranslatePx; + _flingAnimationTargetTranslatePx = _flingAnimationInitialTranslatePx + + pixelsPerSec * flingDistanceMultiplier; + + final flingDuration = new Duration( + milliseconds: + max(200, (pixelsPerSec * flingDurationMultiplier).abs().round())); + + _flingAnimator! + ..duration = flingDuration + ..forward(from: 0.0); + _isFlinging = true; + } + + /// Decelerates a fling event. + double _decelerate(double value) => flingDeceleratorFactor == 1.0 + ? 1.0 - (1.0 - value) * (1.0 - value) + : 1.0 - pow(1.0 - value, 2 * flingDeceleratorFactor); + + /// Updates the chart axis state on each tick of the [AnimationController]. + void _onFlingTick() { + if (!_isFlinging) { + return; + } + + final percent = _flingAnimator!.value; + final deceleratedPercent = _decelerate(percent); + final translation = lerpDouble(_flingAnimationInitialTranslatePx, + _flingAnimationTargetTranslatePx, deceleratedPercent); + + final domainAxis = chart!.domainAxis!; + + domainAxis.setViewportSettings( + domainAxis.viewportScalingFactor, translation!, + drawAreaWidth: chart!.drawAreaBounds.width); + + if (percent >= 1.0) { + stopFlingAnimation(); + onPanEnd(); + chart!.redraw(); + } else { + chart!.redraw(skipAnimation: true, skipLayout: true); + } + } + + /// Stops any current fling animations that may be executing. + void stopFlingAnimation() { + if (_isFlinging) { + _isFlinging = false; + _flingAnimator?.stop(); + } + } +} diff --git a/lib/src/canvas/circle_sector_painter.dart b/lib/src/canvas/circle_sector_painter.dart new file mode 100644 index 0000000..197ef4c --- /dev/null +++ b/lib/src/canvas/circle_sector_painter.dart @@ -0,0 +1,103 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show cos, pi, sin, Point; +import 'package:flutter/material.dart'; +import 'package:charts_common/common.dart' as common show Color; + +/// Draws a sector of a circle, with an optional hole in the center. +class CircleSectorPainter { + /// Draws a sector of a circle, with an optional hole in the center. + /// + /// [center] The x, y coordinates of the circle's center. + /// [radius] The radius of the circle. + /// [innerRadius] Optional radius of a hole in the center of the circle that + /// should not be filled in as part of the sector. + /// [startAngle] The angle at which the arc starts, measured clockwise from + /// the positive x axis and expressed in radians. + /// [endAngle] The angle at which the arc ends, measured clockwise from the + /// positive x axis and expressed in radians. + /// [fill] Fill color for the sector. + /// [stroke] Stroke color of the arc and radius lines. + /// [strokeWidthPx] Stroke width of the arc and radius lines. + static void draw({ + required Canvas canvas, + required Paint paint, + required Point center, + required double radius, + required double innerRadius, + required double startAngle, + required double endAngle, + common.Color? fill, + }) { + paint.color = new Color.fromARGB(fill!.a, fill.r, fill.g, fill.b); + paint.style = PaintingStyle.fill; + + final innerRadiusStartPoint = new Point( + innerRadius * cos(startAngle) + center.x, + innerRadius * sin(startAngle) + center.y); + + final innerRadiusEndPoint = new Point( + innerRadius * cos(endAngle) + center.x, + innerRadius * sin(endAngle) + center.y); + + final radiusStartPoint = new Point( + radius * cos(startAngle) + center.x, + radius * sin(startAngle) + center.y); + + final centerOffset = new Offset(center.x.toDouble(), center.y.toDouble()); + + final isFullCircle = startAngle != null && + endAngle != null && + endAngle - startAngle == 2 * pi; + + final midpointAngle = (endAngle + startAngle) / 2; + + final path = new Path() + ..moveTo(innerRadiusStartPoint.x, innerRadiusStartPoint.y); + + path.lineTo(radiusStartPoint.x, radiusStartPoint.y); + + // For full circles, draw the arc in two parts. + if (isFullCircle) { + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius), + startAngle, midpointAngle - startAngle, true); + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius), + midpointAngle, endAngle - midpointAngle, true); + } else { + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: radius), + startAngle, endAngle - startAngle, true); + } + + path.lineTo(innerRadiusEndPoint.x, innerRadiusEndPoint.y); + + // For full circles, draw the arc in two parts. + if (isFullCircle) { + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius), + endAngle, midpointAngle - endAngle, true); + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius), + midpointAngle, startAngle - midpointAngle, true); + } else { + path.arcTo(new Rect.fromCircle(center: centerOffset, radius: innerRadius), + endAngle, startAngle - endAngle, true); + } + + // Drawing two copies of this line segment, before and after the arcs, + // ensures that the path actually gets closed correctly. + path.lineTo(radiusStartPoint.x, radiusStartPoint.y); + + canvas.drawPath(path, paint); + } +} diff --git a/lib/src/canvas/line_painter.dart b/lib/src/canvas/line_painter.dart new file mode 100644 index 0000000..1867ac3 --- /dev/null +++ b/lib/src/canvas/line_painter.dart @@ -0,0 +1,244 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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:ui' as ui show Shader; +import 'dart:math' show Point, Rectangle; +import 'package:flutter/material.dart'; +import 'package:charts_common/common.dart' as common show Color; + +/// Draws a simple line. +/// +/// Lines may be styled with dash patterns similar to stroke-dasharray in SVG +/// path elements. Dash patterns are currently only supported between vertical +/// or horizontal line segments at this time. +class LinePainter { + /// Draws a simple line. + /// + /// [dashPattern] controls the pattern of dashes and gaps in a line. It is a + /// list of lengths of alternating dashes and gaps. The rendering is similar + /// to stroke-dasharray in SVG path elements. An odd number of values in the + /// pattern will be repeated to derive an even number of values. "1,2,3" is + /// equivalent to "1,2,3,1,2,3." + static void draw( + {required Canvas canvas, + required Paint paint, + required List points, + Rectangle? clipBounds, + common.Color? fill, + common.Color? stroke, + bool? roundEndCaps, + double? strokeWidthPx, + List? dashPattern, + ui.Shader? shader}) { + if (points.isEmpty) { + return; + } + + // Apply clip bounds as a clip region. + if (clipBounds != null) { + canvas + ..save() + ..clipRect(new Rect.fromLTWH( + clipBounds.left.toDouble(), + clipBounds.top.toDouble(), + clipBounds.width.toDouble(), + clipBounds.height.toDouble())); + } + + paint.color = new Color.fromARGB(stroke!.a, stroke.r, stroke.g, stroke.b); + + if (shader != null) { + paint.shader = shader; + } + + // If the line has a single point, draw a circle. + if (points.length == 1) { + final point = points.first; + paint.style = PaintingStyle.fill; + canvas.drawCircle(new Offset(point.x.toDouble(), point.y.toDouble()), + strokeWidthPx ?? 0, paint); + } else { + if (strokeWidthPx != null) { + paint.strokeWidth = strokeWidthPx; + } + paint.strokeJoin = StrokeJoin.round; + paint.style = PaintingStyle.stroke; + + if (dashPattern == null || dashPattern.isEmpty) { + if (roundEndCaps == true) { + paint.strokeCap = StrokeCap.round; + } + + _drawSolidLine(canvas, paint, points); + } else { + _drawDashedLine(canvas, paint, points, dashPattern); + } + } + + if (clipBounds != null) { + canvas.restore(); + } + } + + /// Draws solid lines between each point. + static void _drawSolidLine(Canvas canvas, Paint paint, List points) { + // TODO: Extract a native line component which constructs the + // appropriate underlying data structures to avoid conversion. + final path = new Path() + ..moveTo(points.first.x.toDouble(), points.first.y.toDouble()); + + for (var point in points) { + path.lineTo(point.x.toDouble(), point.y.toDouble()); + } + + canvas.drawPath(path, paint); + } + + /// Draws dashed lines lines between each point. + static void _drawDashedLine( + Canvas canvas, Paint paint, List points, List dashPattern) { + final localDashPattern = new List.from(dashPattern); + + // If an odd number of parts are defined, repeat the pattern to get an even + // number. + if (dashPattern.length % 2 == 1) { + localDashPattern.addAll(dashPattern); + } + + // Stores the previous point in the series. + var previousSeriesPoint = _getOffset(points.first); + + var remainder = 0; + var solid = true; + var dashPatternIndex = 0; + + // Gets the next segment in the dash pattern, looping back to the + // beginning once the end has been reached. + var getNextDashPatternSegment = () { + final dashSegment = localDashPattern[dashPatternIndex]; + dashPatternIndex = (dashPatternIndex + 1) % localDashPattern.length; + return dashSegment; + }; + + // Array of points that is used to draw a connecting path when only a + // partial dash pattern segment can be drawn in the remaining length of a + // line segment (between two defined points in the shape). + var remainderPoints; + + // Draw the path through all the rest of the points in the series. + for (var pointIndex = 1; pointIndex < points.length; pointIndex++) { + // Stores the current point in the series. + final seriesPoint = _getOffset(points[pointIndex]); + + if (previousSeriesPoint == seriesPoint) { + // Bypass dash pattern handling if the points are the same. + } else { + // Stores the previous point along the current series line segment where + // we rendered a dash (or left a gap). + var previousPoint = previousSeriesPoint; + + var d = _getOffsetDistance(previousSeriesPoint, seriesPoint); + + while (d > 0) { + var dashSegment = + remainder > 0 ? remainder : getNextDashPatternSegment(); + remainder = 0; + + // Create a unit vector in the direction from previous to next point. + final v = seriesPoint - previousPoint; + final u = new Offset(v.dx / v.distance, v.dy / v.distance); + + // If the remaining distance is less than the length of the dash + // pattern segment, then cut off the pattern segment for this portion + // of the overall line. + final distance = d < dashSegment ? d : dashSegment.toDouble(); + + // Compute a vector representing the length of dash pattern segment to + // be drawn. + final nextPoint = previousPoint + (u * distance); + + // If we are in a solid portion of the dash pattern, draw a line. + // Else, move on. + if (solid) { + if (remainderPoints != null) { + // If we had a partial un-drawn dash from the previous point along + // the line, draw a path that includes it and the end of the dash + // pattern segment in the current line segment. + remainderPoints.add(new Offset(nextPoint.dx, nextPoint.dy)); + + final path = new Path() + ..moveTo(remainderPoints.first.dx, remainderPoints.first.dy); + + for (var p in remainderPoints) { + path.lineTo(p.dx, p.dy); + } + + canvas.drawPath(path, paint); + + remainderPoints = null; + } else { + if (d < dashSegment && pointIndex < points.length - 1) { + // If the remaining distance d is too small to fit this dash, + // and we have more points in the line, save off a series of + // remainder points so that we can draw a path segment moving in + // the direction of the next point. + // + // Note that we don't need to save anything off for the "blank" + // portions of the pattern because we still take the remaining + // distance into account before starting the next dash in the + // next line segment. + remainderPoints = [ + new Offset(previousPoint.dx, previousPoint.dy), + new Offset(nextPoint.dx, nextPoint.dy) + ]; + } else { + // Otherwise, draw a simple line segment for this dash. + canvas.drawLine(previousPoint, nextPoint, paint); + } + } + } + + solid = !solid; + previousPoint = nextPoint; + d = d - dashSegment; + } + + // Save off the remaining distance so that we can continue the dash (or + // gap) into the next line segment. + remainder = -d.round(); + + // If we have a remaining un-drawn distance for the current dash (or + // gap), revert the last change to "solid" so that we will continue + // either drawing a dash or leaving a gap. + if (remainder > 0) { + solid = !solid; + } + } + + previousSeriesPoint = seriesPoint; + } + } + + /// Converts a [Point] into an [Offset]. + static Offset _getOffset(Point point) => + new Offset(point.x.toDouble(), point.y.toDouble()); + + /// Computes the distance between two [Offset]s, as if they were [Point]s. + static num _getOffsetDistance(Offset o1, Offset o2) { + final p1 = new Point(o1.dx, o1.dy); + final p2 = new Point(o2.dx, o2.dy); + return p1.distanceTo(p2); + } +} diff --git a/lib/src/canvas/pie_painter.dart b/lib/src/canvas/pie_painter.dart new file mode 100644 index 0000000..9963261 --- /dev/null +++ b/lib/src/canvas/pie_painter.dart @@ -0,0 +1,84 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show cos, sin, Point; +import 'package:flutter/material.dart'; +import 'package:charts_common/common.dart' as common show CanvasPie; +import 'circle_sector_painter.dart' show CircleSectorPainter; + +/// Draws a pie chart, with an optional hole in the center. +class PiePainter { + /// Draws a pie chart, with an optional hole in the center. + static void draw(Canvas canvas, Paint paint, common.CanvasPie canvasPie) { + final center = canvasPie.center; + final radius = canvasPie.radius; + final innerRadius = canvasPie.innerRadius; + + for (var slice in canvasPie.slices) { + CircleSectorPainter.draw( + canvas: canvas, + paint: paint, + center: center, + radius: radius, + innerRadius: innerRadius, + startAngle: slice.startAngle, + endAngle: slice.endAngle, + fill: slice.fill); + } + + // Draw stroke lines between pie slices. This is done after the slices are + // drawn to ensure that they appear on top. + if (canvasPie.stroke != null && + canvasPie.strokeWidthPx != null && + canvasPie.slices.length > 1) { + paint.color = new Color.fromARGB(canvasPie.stroke!.a, canvasPie.stroke!.r, + canvasPie.stroke!.g, canvasPie.stroke!.b); + + paint.strokeWidth = canvasPie.strokeWidthPx; + paint.strokeJoin = StrokeJoin.bevel; + paint.style = PaintingStyle.stroke; + + final path = new Path(); + + for (var slice in canvasPie.slices) { + final innerRadiusStartPoint = new Point( + innerRadius * cos(slice.startAngle) + center.x, + innerRadius * sin(slice.startAngle) + center.y); + + final innerRadiusEndPoint = new Point( + innerRadius * cos(slice.endAngle) + center.x, + innerRadius * sin(slice.endAngle) + center.y); + + final radiusStartPoint = new Point( + radius * cos(slice.startAngle) + center.x, + radius * sin(slice.startAngle) + center.y); + + final radiusEndPoint = new Point( + radius * cos(slice.endAngle) + center.x, + radius * sin(slice.endAngle) + center.y); + + path.moveTo(innerRadiusStartPoint.x, innerRadiusStartPoint.y); + + path.lineTo(radiusStartPoint.x, radiusStartPoint.y); + + path.moveTo(innerRadiusEndPoint.x, innerRadiusEndPoint.y); + + path.lineTo(radiusEndPoint.x, radiusEndPoint.y); + } + + canvas.drawPath(path, paint); + } + } +} diff --git a/lib/src/canvas/point_painter.dart b/lib/src/canvas/point_painter.dart new file mode 100644 index 0000000..fe3eab0 --- /dev/null +++ b/lib/src/canvas/point_painter.dart @@ -0,0 +1,56 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point; +import 'package:flutter/material.dart'; +import 'package:charts_common/common.dart' as common show Color; + +/// Draws a simple point. +/// +/// TODO: Support for more shapes than circles? +class PointPainter { + static void draw( + {required Canvas canvas, + required Paint paint, + required Point point, + required double radius, + common.Color? fill, + common.Color? stroke, + double? strokeWidthPx}) { + if (point == null) { + return; + } + + if (fill != null) { + paint.color = new Color.fromARGB(fill.a, fill.r, fill.g, fill.b); + paint.style = PaintingStyle.fill; + + canvas.drawCircle( + new Offset(point.x.toDouble(), point.y.toDouble()), radius, paint); + } + + // [Canvas.drawCircle] does not support drawing a circle with both a fill + // and a stroke at this time. Use a separate circle for the stroke. + if (stroke != null && strokeWidthPx != null && strokeWidthPx > 0.0) { + paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b); + paint.strokeWidth = strokeWidthPx; + paint.strokeJoin = StrokeJoin.bevel; + paint.style = PaintingStyle.stroke; + + canvas.drawCircle( + new Offset(point.x.toDouble(), point.y.toDouble()), radius, paint); + } + } +} diff --git a/lib/src/canvas/polygon_painter.dart b/lib/src/canvas/polygon_painter.dart new file mode 100644 index 0000000..5d1b12f --- /dev/null +++ b/lib/src/canvas/polygon_painter.dart @@ -0,0 +1,99 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Point, Rectangle; +import 'package:flutter/material.dart'; +import 'package:charts_common/common.dart' as common show Color; + +/// Draws a simple line. +/// +/// Lines may be styled with dash patterns similar to stroke-dasharray in SVG +/// path elements. Dash patterns are currently only supported between vertical +/// or horizontal line segments at this time. +class PolygonPainter { + /// Draws a simple line. + /// + /// [dashPattern] controls the pattern of dashes and gaps in a line. It is a + /// list of lengths of alternating dashes and gaps. The rendering is similar + /// to stroke-dasharray in SVG path elements. An odd number of values in the + /// pattern will be repeated to derive an even number of values. "1,2,3" is + /// equivalent to "1,2,3,1,2,3." + static void draw( + {required Canvas canvas, + required Paint paint, + required List points, + Rectangle? clipBounds, + common.Color? fill, + common.Color? stroke, + double? strokeWidthPx}) { + if (points.isEmpty) { + return; + } + + // Apply clip bounds as a clip region. + if (clipBounds != null) { + canvas + ..save() + ..clipRect(new Rect.fromLTWH( + clipBounds.left.toDouble(), + clipBounds.top.toDouble(), + clipBounds.width.toDouble(), + clipBounds.height.toDouble())); + } + + final strokeColor = stroke != null + ? new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b) + : null; + + final fillColor = fill != null + ? new Color.fromARGB(fill.a, fill.r, fill.g, fill.b) + : null; + + // If the line has a single point, draw a circle. + if (points.length == 1) { + final point = points.first; + if (fillColor != null) { + paint.color = fillColor; + } + paint.style = PaintingStyle.fill; + canvas.drawCircle(new Offset(point.x.toDouble(), point.y.toDouble()), + strokeWidthPx!, paint); + } else { + if (strokeColor != null && strokeWidthPx != null) { + paint.strokeWidth = strokeWidthPx; + paint.strokeJoin = StrokeJoin.bevel; + paint.style = PaintingStyle.stroke; + } + + if (fillColor != null) { + paint.color = fillColor; + paint.style = PaintingStyle.fill; + } + + final path = new Path() + ..moveTo(points.first.x.toDouble(), points.first.y.toDouble()); + + for (var point in points) { + path.lineTo(point.x.toDouble(), point.y.toDouble()); + } + + canvas.drawPath(path, paint); + } + + if (clipBounds != null) { + canvas.restore(); + } + } +} diff --git a/lib/src/cartesian_chart.dart b/lib/src/cartesian_chart.dart new file mode 100644 index 0000000..6e789fb --- /dev/null +++ b/lib/src/cartesian_chart.dart @@ -0,0 +1,122 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection' show LinkedHashMap; +import 'package:meta/meta.dart' show immutable, protected; + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + BaseChart, + CartesianChart, + NumericAxis, + NumericAxisSpec, + RTLSpec, + Series, + SeriesRendererConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'base_chart.dart' show BaseChart, LayoutConfig; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +@immutable +abstract class CartesianChart extends BaseChart { + final common.AxisSpec? domainAxis; + final common.NumericAxisSpec? primaryMeasureAxis; + final common.NumericAxisSpec? secondaryMeasureAxis; + final LinkedHashMap? disjointMeasureAxes; + final bool? flipVerticalAxis; + + CartesianChart( + List> seriesList, { + bool? animate, + Duration? animationDuration, + this.domainAxis, + this.primaryMeasureAxis, + this.secondaryMeasureAxis, + this.disjointMeasureAxes, + common.SeriesRendererConfig? defaultRenderer, + List>? customSeriesRenderers, + List>? behaviors, + List>? selectionModels, + common.RTLSpec? rtlSpec, + bool defaultInteractions = true, + LayoutConfig? layoutConfig, + UserManagedState? userManagedState, + this.flipVerticalAxis, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + defaultInteractions: defaultInteractions, + layoutConfig: layoutConfig, + userManagedState: userManagedState, + ); + + @override + void updateCommonChart(common.BaseChart baseChart, BaseChart? oldWidget, + BaseChartState chartState) { + super.updateCommonChart(baseChart, oldWidget, chartState); + + final prev = oldWidget as CartesianChart?; + final chart = baseChart as common.CartesianChart; + + if (flipVerticalAxis != null) { + chart.flipVerticalAxisOutput = flipVerticalAxis!; + } + + if (domainAxis != null && domainAxis != prev?.domainAxis) { + chart.domainAxisSpec = domainAxis!; + chartState.markChartDirty(); + } + + if (primaryMeasureAxis != prev?.primaryMeasureAxis) { + chart.primaryMeasureAxisSpec = primaryMeasureAxis; + chartState.markChartDirty(); + } + + if (secondaryMeasureAxis != prev?.secondaryMeasureAxis) { + chart.secondaryMeasureAxisSpec = secondaryMeasureAxis; + chartState.markChartDirty(); + } + + if (disjointMeasureAxes != prev?.disjointMeasureAxes) { + chart.disjointMeasureAxisSpecs = disjointMeasureAxes; + chartState.markChartDirty(); + } + } + + @protected + LinkedHashMap? createDisjointMeasureAxes() { + if (disjointMeasureAxes != null) { + final disjointAxes = new LinkedHashMap(); + + disjointMeasureAxes! + .forEach((String axisId, common.NumericAxisSpec axisSpec) { + disjointAxes[axisId] = axisSpec.createAxis(); + }); + + return disjointAxes; + } else { + return null; + } + } +} diff --git a/lib/src/chart_canvas.dart b/lib/src/chart_canvas.dart new file mode 100644 index 0000000..77b7d77 --- /dev/null +++ b/lib/src/chart_canvas.dart @@ -0,0 +1,442 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:ui' as ui show Gradient, Shader; +import 'dart:math' show Point, Rectangle, max; +import 'package:charts_common/common.dart' as common + show + BlendMode, + ChartCanvas, + CanvasBarStack, + CanvasPie, + Color, + FillPatternType, + GraphicsFactory, + Link, + LinkOrientation, + StyleFactory, + TextElement, + TextDirection; +import 'package:flutter/material.dart'; +import 'text_element.dart' show TextElement; +import 'canvas/circle_sector_painter.dart' show CircleSectorPainter; +import 'canvas/line_painter.dart' show LinePainter; +import 'canvas/pie_painter.dart' show PiePainter; +import 'canvas/point_painter.dart' show PointPainter; +import 'canvas/polygon_painter.dart' show PolygonPainter; + +class ChartCanvas implements common.ChartCanvas { + /// Pixels to allow to overdraw above the draw area that fades to transparent. + static const double rect_top_gradient_pixels = 5; + + final Canvas canvas; + final common.GraphicsFactory graphicsFactory; + final _paint = new Paint(); + + ChartCanvas(this.canvas, this.graphicsFactory); + + @override + void drawCircleSector(Point center, double radius, double innerRadius, + double startAngle, double endAngle, + {common.Color? fill, common.Color? stroke, double? strokeWidthPx}) { + CircleSectorPainter.draw( + canvas: canvas, + paint: _paint, + center: center, + radius: radius, + innerRadius: innerRadius, + startAngle: startAngle, + endAngle: endAngle, + fill: fill, + ); + } + + @override + void drawLink( + common.Link link, common.LinkOrientation orientation, common.Color fill) { + // TODO: Implement drawLink for flutter. + throw ("Flutter drawLink() has not been implemented."); + } + + @override + void drawLine( + {required List points, + Rectangle? clipBounds, + common.Color? fill, + common.Color? stroke, + bool? roundEndCaps, + double? strokeWidthPx, + List? dashPattern}) { + LinePainter.draw( + canvas: canvas, + paint: _paint, + points: points, + clipBounds: clipBounds, + fill: fill, + stroke: stroke, + roundEndCaps: roundEndCaps, + strokeWidthPx: strokeWidthPx, + dashPattern: dashPattern); + } + + @override + void drawPie(common.CanvasPie canvasPie) { + PiePainter.draw(canvas, _paint, canvasPie); + } + + @override + void drawPoint( + {required Point point, + required double radius, + common.Color? fill, + common.Color? stroke, + double? strokeWidthPx, + common.BlendMode? blendMode}) { + PointPainter.draw( + canvas: canvas, + paint: _paint, + point: point, + radius: radius, + fill: fill, + stroke: stroke, + strokeWidthPx: strokeWidthPx); + } + + @override + void drawPolygon( + {required List points, + Rectangle? clipBounds, + common.Color? fill, + common.Color? stroke, + double? strokeWidthPx}) { + PolygonPainter.draw( + canvas: canvas, + paint: _paint, + points: points, + clipBounds: clipBounds, + fill: fill, + stroke: stroke, + strokeWidthPx: strokeWidthPx); + } + + /// Creates a bottom to top gradient that transitions [fill] to transparent. + ui.Gradient _createHintGradient(double left, double top, common.Color fill) { + return new ui.Gradient.linear( + new Offset(left, top), + new Offset(left, top - rect_top_gradient_pixels), + [ + new Color.fromARGB(fill.a, fill.r, fill.g, fill.b), + new Color.fromARGB(0, fill.r, fill.g, fill.b) + ], + ); + } + + @override + void drawRect(Rectangle bounds, + {common.Color? fill, + common.FillPatternType? pattern, + common.Color? stroke, + double? strokeWidthPx, + Rectangle? drawAreaBounds}) { + // TODO: remove this explicit `bool` type when no longer needed + // to work around https://github.com/dart-lang/language/issues/1785 + final bool drawStroke = + (strokeWidthPx != null && strokeWidthPx > 0.0 && stroke != null); + + final strokeWidthOffset = (drawStroke ? strokeWidthPx : 0); + + // Factor out stroke width, if a stroke is enabled. + final fillRectBounds = new Rectangle( + bounds.left + strokeWidthOffset / 2, + bounds.top + strokeWidthOffset / 2, + bounds.width - strokeWidthOffset, + bounds.height - strokeWidthOffset); + + switch (pattern) { + case common.FillPatternType.forwardHatch: + _drawForwardHatchPattern(fillRectBounds, canvas, + fill: fill!, drawAreaBounds: drawAreaBounds); + break; + + case common.FillPatternType.solid: + default: + // Use separate rect for drawing stroke + _paint.color = new Color.fromARGB(fill!.a, fill.r, fill.g, fill.b); + _paint.style = PaintingStyle.fill; + + // Apply a gradient to the top [rect_top_gradient_pixels] to transparent + // if the rectangle is higher than the [drawAreaBounds] top. + if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) { + _paint.shader = _createHintGradient(drawAreaBounds.left.toDouble(), + drawAreaBounds.top.toDouble(), fill); + } + + canvas.drawRect(_getRect(fillRectBounds), _paint); + break; + } + + // [Canvas.drawRect] does not support drawing a rectangle with both a fill + // and a stroke at this time. Use a separate rect for the stroke. + if (drawStroke) { + _paint.color = new Color.fromARGB(stroke.a, stroke.r, stroke.g, stroke.b); + // Set shader to null if no draw area bounds so it can use the color + // instead. + _paint.shader = drawAreaBounds != null + ? _createHintGradient(drawAreaBounds.left.toDouble(), + drawAreaBounds.top.toDouble(), stroke) + : null; + _paint.strokeJoin = StrokeJoin.round; + _paint.strokeWidth = strokeWidthPx; + _paint.style = PaintingStyle.stroke; + + canvas.drawRect(_getRect(bounds), _paint); + } + + // Reset the shader. + _paint.shader = null; + } + + @override + void drawRRect(Rectangle bounds, + {common.Color? fill, + common.Color? stroke, + common.Color? patternColor, + common.FillPatternType? fillPattern, + double? patternStrokeWidthPx, + double? strokeWidthPx, + num? radius, + bool roundTopLeft = false, + bool roundTopRight = false, + bool roundBottomLeft = false, + bool roundBottomRight = false}) { + // Use separate rect for drawing stroke + _paint.color = new Color.fromARGB(fill!.a, fill.r, fill.g, fill.b); + _paint.style = PaintingStyle.fill; + + canvas.drawRRect( + _getRRect(bounds, + radius: radius?.toDouble() ?? 0.0, + roundTopLeft: roundTopLeft, + roundTopRight: roundTopRight, + roundBottomLeft: roundBottomLeft, + roundBottomRight: roundBottomRight), + _paint); + } + + @override + void drawBarStack(common.CanvasBarStack barStack, + {Rectangle? drawAreaBounds}) { + // only clip if rounded rect. + + // Clip a rounded rect for the whole region if rounded bars. + final roundedCorners = barStack.radius != null && 0 < barStack.radius!; + + if (roundedCorners) { + canvas + ..save() + ..clipRRect(_getRRect( + barStack.fullStackRect, + radius: barStack.radius!.toDouble(), + roundTopLeft: barStack.roundTopLeft, + roundTopRight: barStack.roundTopRight, + roundBottomLeft: barStack.roundBottomLeft, + roundBottomRight: barStack.roundBottomRight, + )); + } + + // Draw each bar. + for (var barIndex = 0; barIndex < barStack.segments.length; barIndex++) { + // TODO: Add configuration for hiding stack line. + // TODO: Don't draw stroke on bottom of bars. + final segment = barStack.segments[barIndex]; + drawRect(segment.bounds, + fill: segment.fill, + pattern: segment.pattern, + stroke: segment.stroke, + strokeWidthPx: segment.strokeWidthPx, + drawAreaBounds: drawAreaBounds); + } + + if (roundedCorners) { + canvas.restore(); + } + } + + @override + void drawText(common.TextElement textElement, int offsetX, int offsetY, + {double rotation = 0.0}) { + // Must be Flutter TextElement. + assert(textElement is TextElement); + + final flutterTextElement = textElement as TextElement; + final textDirection = flutterTextElement.textDirection; + final measurement = flutterTextElement.measurement; + + if (rotation != 0) { + // TODO: Remove once textAnchor works. + if (textDirection == common.TextDirection.rtl) { + offsetY += measurement.horizontalSliceWidth.toInt(); + } + + offsetX -= flutterTextElement.verticalFontShift; + + canvas.save(); + canvas.translate(offsetX.toDouble(), offsetY.toDouble()); + canvas.rotate(rotation); + + textElement.textPainter!.paint(canvas, new Offset(0.0, 0.0)); + + canvas.restore(); + } else { + // TODO: Remove once textAnchor works. + if (textDirection == common.TextDirection.rtl) { + offsetX -= measurement.horizontalSliceWidth.toInt(); + } + + // Account for missing center alignment. + if (textDirection == common.TextDirection.center) { + offsetX -= (measurement.horizontalSliceWidth / 2).ceil(); + } + + offsetY -= flutterTextElement.verticalFontShift; + + textElement.textPainter! + .paint(canvas, new Offset(offsetX.toDouble(), offsetY.toDouble())); + } + } + + @override + void setClipBounds(Rectangle clipBounds) { + canvas + ..save() + ..clipRect(_getRect(clipBounds)); + } + + @override + void resetClipBounds() { + canvas.restore(); + } + + /// Convert dart:math [Rectangle] to Flutter [Rect]. + Rect _getRect(Rectangle rectangle) { + return new Rect.fromLTWH( + rectangle.left.toDouble(), + rectangle.top.toDouble(), + rectangle.width.toDouble(), + rectangle.height.toDouble()); + } + + /// Convert dart:math [Rectangle] and to Flutter [RRect]. + RRect _getRRect( + Rectangle rectangle, { + double radius = 0, + bool roundTopLeft = false, + bool roundTopRight = false, + bool roundBottomLeft = false, + bool roundBottomRight = false, + }) { + final cornerRadius = + radius == 0 ? Radius.zero : new Radius.circular(radius); + + return new RRect.fromLTRBAndCorners( + rectangle.left.toDouble(), + rectangle.top.toDouble(), + rectangle.right.toDouble(), + rectangle.bottom.toDouble(), + topLeft: roundTopLeft ? cornerRadius : Radius.zero, + topRight: roundTopRight ? cornerRadius : Radius.zero, + bottomLeft: roundBottomLeft ? cornerRadius : Radius.zero, + bottomRight: roundBottomRight ? cornerRadius : Radius.zero); + } + + /// Draws a forward hatch pattern in the given bounds. + _drawForwardHatchPattern( + Rectangle bounds, + Canvas canvas, { + common.Color? background, + common.Color? fill, + double fillWidthPx = 4.0, + Rectangle? drawAreaBounds, + }) { + background ??= common.StyleFactory.style.white; + fill ??= common.StyleFactory.style.black; + + // Fill in the shape with a solid background color. + _paint.color = new Color.fromARGB( + background.a, background.r, background.g, background.b); + _paint.style = PaintingStyle.fill; + + // Apply a gradient the background if bounds exceed the draw area. + if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) { + _paint.shader = _createHintGradient(drawAreaBounds.left.toDouble(), + drawAreaBounds.top.toDouble(), background); + } + + canvas.drawRect(_getRect(bounds), _paint); + + // As a simplification, we will treat the bounds as a large square and fill + // it up with lines from the bottom-left corner to the top-right corner. + // Get the longer side of the bounds here for the size of this square. + final size = max(bounds.width, bounds.height); + + final x0 = bounds.left + size + fillWidthPx; + final x1 = bounds.left - fillWidthPx; + final y0 = bounds.bottom - size - fillWidthPx; + final y1 = bounds.bottom + fillWidthPx; + final offset = 8; + + final isVertical = bounds.height >= bounds.width; + + // The "first" line segment will be drawn from the bottom left corner of the + // bounds, up and towards the right. Start the loop N iterations "back" to + // draw partial line segments beneath (or to the left) of this segment, + // where N is the number of offsets that fit inside the smaller dimension of + // the bounds. + final smallSide = isVertical ? bounds.width : bounds.height; + final start = -(smallSide / offset).round() * offset; + + // Keep going until we reach the top or right of the bounds, depending on + // whether the rectangle is oriented vertically or horizontally. + final end = size + offset; + + // Create gradient for line painter if top bounds exceeded. + ui.Shader? lineShader; + if (drawAreaBounds != null && bounds.top < drawAreaBounds.top) { + lineShader = _createHintGradient( + drawAreaBounds.left.toDouble(), drawAreaBounds.top.toDouble(), fill); + } + + for (int i = start; i < end; i = i + offset) { + // For vertical bounds, we need to draw lines from top to bottom. For + // bounds, we need to draw lines from left to right. + final modifier = isVertical ? -1 * i : i; + + // Draw a line segment in the bottom right corner of the pattern. + LinePainter.draw( + canvas: canvas, + paint: _paint, + points: [ + new Point(x0 + modifier, y0), + new Point(x1 + modifier, y1), + ], + stroke: fill, + strokeWidthPx: fillWidthPx, + shader: lineShader); + } + } + + @override + set drawingView(String? viewName) {} +} diff --git a/lib/src/chart_container.dart b/lib/src/chart_container.dart new file mode 100644 index 0000000..b10f533 --- /dev/null +++ b/lib/src/chart_container.dart @@ -0,0 +1,379 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show + A11yNode, + AxisDirection, + BaseChart, + ChartContext, + DateTimeFactory, + LocalDateTimeFactory, + ProxyGestureListener, + RTLSpec, + SelectionModelType, + Series, + Performance; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; + +import 'base_chart.dart' show BaseChart; +import 'base_chart_state.dart' show BaseChartState; +import 'chart_canvas.dart' show ChartCanvas; +import 'graphics_factory.dart' show GraphicsFactory; +import 'time_series_chart.dart' show TimeSeriesChart; +import 'user_managed_state.dart' show UserManagedState; + +/// Widget that inflates to a [CustomPaint] that implements common [ChartContext]. +class ChartContainer extends CustomPaint { + final BaseChart chartWidget; + final BaseChart? oldChartWidget; + final BaseChartState chartState; + final double animationValue; + final bool rtl; + final common.RTLSpec? rtlSpec; + final UserManagedState? userManagedState; + + ChartContainer( + {this.oldChartWidget, + required this.chartWidget, + required this.chartState, + required this.animationValue, + required this.rtl, + this.rtlSpec, + this.userManagedState}); + + @override + RenderCustomPaint createRenderObject(BuildContext context) { + return new ChartContainerRenderObject()..reconfigure(this, context); + } + + @override + void updateRenderObject( + BuildContext context, ChartContainerRenderObject renderObject) { + renderObject.reconfigure(this, context); + } +} + +/// [RenderCustomPaint] that implements common [ChartContext]. +class ChartContainerRenderObject extends RenderCustomPaint + implements common.ChartContext { + common.BaseChart? _chart; + List>? _seriesList; + late BaseChartState _chartState; + bool _chartContainerIsRtl = false; + common.RTLSpec? _rtlSpec; + common.DateTimeFactory? _dateTimeFactory; + bool _exploreMode = false; + List? _a11yNodes; + + void reconfigure(ChartContainer config, BuildContext context) { + _chartState = config.chartState; + + _dateTimeFactory = (config.chartWidget is TimeSeriesChart) + ? (config.chartWidget as TimeSeriesChart).dateTimeFactory + : null; + _dateTimeFactory ??= new common.LocalDateTimeFactory(); + + if (_chart == null) { + common.Performance.time('chartsCreate'); + _chart = config.chartWidget.createCommonChart(_chartState); + _chart!.init(this, new GraphicsFactory(context)); + common.Performance.timeEnd('chartsCreate'); + } + common.Performance.time('chartsConfig'); + config.chartWidget + .updateCommonChart(_chart!, config.oldChartWidget, _chartState); + + _rtlSpec = config.rtlSpec; + _chartContainerIsRtl = config.rtl; + + common.Performance.timeEnd('chartsConfig'); + + if (_chartState.chartIsDirty) { + _chart!.configurationChanged(); + } + + // If series list changes or other configuration changed that triggered the + // _chartState.configurationChanged flag to be set (such as axis, behavior, + // and renderer changes). Otherwise, the chart only requests repainting and + // does not reprocess the series. + // + // Series list is considered "changed" based on the instance. + if (_seriesList != config.chartWidget.seriesList || + _chartState.chartIsDirty) { + _chartState.resetChartDirtyFlag(); + _seriesList = config.chartWidget.seriesList; + + // Clear out the a11y nodes generated. + _a11yNodes = null; + + common.Performance.time('chartsDraw'); + _chart!.draw(_seriesList!); + common.Performance.timeEnd('chartsDraw'); + + // This is needed because when a series changes we need to reset flutter's + // animation value from 1.0 back to 0.0. + _chart!.animationPercent = 0.0; + markNeedsLayout(); + } else { + _chart!.animationPercent = config.animationValue; + markNeedsPaint(); + } + + _updateUserManagedState(config.userManagedState); + + // Set the painter used for calling common chart for paint. + // This painter is also used to generate semantic nodes for a11y. + _setNewPainter(); + } + + /// If user managed state is set, check each setting to see if it is different + /// than internal chart state and only update if different. + _updateUserManagedState(UserManagedState? newState) { + if (newState == null) { + return; + } + + // Only override the selection model if it is different than the existing + // selection model so update listeners are not unnecessarily triggered. + for (common.SelectionModelType type in newState.selectionModels.keys) { + final model = _chart!.getSelectionModel(type); + + final userModel = + newState.selectionModels[type]!.getModel(_chart!.currentSeriesList); + + if (model != userModel) { + model.updateSelection( + userModel.selectedDatum, userModel.selectedSeries); + } + } + } + + @override + void performLayout() { + common.Performance.time('chartsLayout'); + _chart! + .measure(constraints.maxWidth.toInt(), constraints.maxHeight.toInt()); + _chart!.layout(constraints.maxWidth.toInt(), constraints.maxHeight.toInt()); + common.Performance.timeEnd('chartsLayout'); + size = constraints.biggest; + + // Check if the gestures registered in gesture registry matches what the + // common chart is listening to. + // TODO: Still need a test for this for sanity sake. +// assert(_desiredGestures +// .difference(_chart!.gestureProxy.listenedGestures) +// .isEmpty); + } + + @override + void markNeedsLayout() { + super.markNeedsLayout(); + if (parent != null) { + markParentNeedsLayout(); + } + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + void requestRedraw() {} + + @override + void requestAnimation(Duration transition) { + void startAnimationController(_) { + _chartState.setAnimation(transition); + } + + // Sometimes chart behaviors try to draw the chart outside of a Flutter draw + // cycle. Schedule a frame manually to handle these cases. + if (!SchedulerBinding.instance!.hasScheduledFrame) { + SchedulerBinding.instance!.scheduleFrame(); + } + + SchedulerBinding.instance!.addPostFrameCallback(startAnimationController); + } + + /// Request Flutter to rebuild the widget/container of chart. + /// + /// This is different than requesting redraw and paint because those only + /// affect the chart widget. This is for requesting rebuild of the Flutter + /// widget that contains the chart widget. This is necessary for supporting + /// Flutter widgets that are layout with the chart. + /// + /// Example is legends, a legend widget can be layout on top of the chart + /// widget or along the sides of the chart. Requesting a rebuild allows + /// the legend to layout and redraw itself. + void requestRebuild() { + void doRebuild(_) { + _chartState.requestRebuild(); + } + + // Flutter does not allow requesting rebuild during the build cycle, this + // schedules rebuild request to happen after the current build cycle. + // This is needed to request rebuild after the legend has been added in the + // post process phase of the chart, which happens during the chart widget's + // build cycle. + SchedulerBinding.instance!.addPostFrameCallback(doRebuild); + } + + /// When Flutter's markNeedsLayout is called, layout and paint are both + /// called. If animations are off, Flutter's paint call after layout will + /// paint the chart. If animations are on, Flutter's paint is called with the + /// initial animation value and then the animation controller is started after + /// this first build cycle. + @override + void requestPaint() { + markNeedsPaint(); + } + + @override + double get pixelsPerDp => 1.0; + + @override + bool get chartContainerIsRtl => _chartContainerIsRtl; + + @override + common.RTLSpec? get rtlSpec => _rtlSpec; + + @override + bool get isRtl => + _chartContainerIsRtl && + (_rtlSpec == null || + _rtlSpec?.axisDirection == common.AxisDirection.reversed); + + @override + bool get isTappable => _chart!.isTappable; + + @override + common.DateTimeFactory get dateTimeFactory => _dateTimeFactory!; + + /// Gets the chart's gesture listener. + common.ProxyGestureListener get gestureProxy => _chart!.gestureProxy; + + TextDirection get textDirection => + _chartContainerIsRtl ? TextDirection.rtl : TextDirection.ltr; + + @override + void enableA11yExploreMode(List nodes, + {String? announcement}) { + _a11yNodes = nodes; + _exploreMode = true; + _setNewPainter(); + requestRebuild(); + if (announcement != null) { + SemanticsService.announce(announcement, textDirection); + } + } + + @override + void disableA11yExploreMode({String? announcement}) { + _a11yNodes = []; + _exploreMode = false; + _setNewPainter(); + requestRebuild(); + if (announcement != null) { + SemanticsService.announce(announcement, textDirection); + } + } + + void _setNewPainter() { + painter = new ChartContainerCustomPaint( + oldPainter: painter as ChartContainerCustomPaint?, + chart: _chart!, + exploreMode: _exploreMode, + a11yNodes: _a11yNodes ?? [], + textDirection: textDirection); + } +} + +class ChartContainerCustomPaint extends CustomPainter { + final common.BaseChart chart; + final bool exploreMode; + final List a11yNodes; + final TextDirection textDirection; + + factory ChartContainerCustomPaint( + {ChartContainerCustomPaint? oldPainter, + required common.BaseChart chart, + bool exploreMode = false, + List a11yNodes = const [], + TextDirection textDirection = TextDirection.ltr}) { + if (oldPainter != null && + oldPainter.exploreMode == exploreMode && + oldPainter.a11yNodes == a11yNodes && + oldPainter.textDirection == textDirection) { + return oldPainter; + } else { + return new ChartContainerCustomPaint._internal( + chart: chart, + exploreMode: exploreMode, + a11yNodes: a11yNodes, + textDirection: textDirection); + } + } + + ChartContainerCustomPaint._internal( + {required this.chart, + required this.exploreMode, + required this.a11yNodes, + required this.textDirection}); + + @override + void paint(Canvas canvas, Size size) { + common.Performance.time('chartsPaint'); + final chartsCanvas = new ChartCanvas(canvas, chart.graphicsFactory!); + chart.paint(chartsCanvas); + common.Performance.timeEnd('chartsPaint'); + } + + /// Common chart requests rebuild that handle repaint requests. + @override + bool shouldRepaint(ChartContainerCustomPaint oldPainter) => false; + + /// Rebuild semantics when explore mode is toggled semantic properties change. + @override + bool shouldRebuildSemantics(ChartContainerCustomPaint oldDelegate) { + return exploreMode != oldDelegate.exploreMode || + a11yNodes != oldDelegate.a11yNodes || + textDirection != textDirection; + } + + @override + SemanticsBuilderCallback get semanticsBuilder => _buildSemantics; + + List _buildSemantics(Size size) { + final nodes = []; + + for (common.A11yNode node in a11yNodes) { + final rect = new Rect.fromLTWH( + node.boundingBox.left.toDouble(), + node.boundingBox.top.toDouble(), + node.boundingBox.width.toDouble(), + node.boundingBox.height.toDouble()); + nodes.add(new CustomPainterSemantics( + rect: rect, + properties: new SemanticsProperties( + value: node.label, + textDirection: textDirection, + onDidGainAccessibilityFocus: node.onFocus))); + } + + return nodes; + } +} diff --git a/lib/src/chart_gesture_detector.dart b/lib/src/chart_gesture_detector.dart new file mode 100644 index 0000000..c8c8b9d --- /dev/null +++ b/lib/src/chart_gesture_detector.dart @@ -0,0 +1,140 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async' show Timer; +import 'dart:math' show Point; +import 'package:flutter/material.dart' + show + BuildContext, + GestureDetector, + RenderBox, + ScaleEndDetails, + ScaleStartDetails, + ScaleUpdateDetails, + TapDownDetails, + TapUpDetails; + +import 'behaviors/chart_behavior.dart' show GestureType; +import 'chart_container.dart' show ChartContainer, ChartContainerRenderObject; +import 'util.dart' show getChartContainerRenderObject; + +// From https://docs.flutter.io/flutter/gestures/kLongPressTimeout-constant.html +const Duration _kLongPressTimeout = const Duration(milliseconds: 500); + +class ChartGestureDetector { + bool _listeningForLongPress = false; + + bool _isDragging = false; + + Timer? _longPressTimer; + Point? _lastTapPoint; + double? _lastScale; + + late _ContainerResolver _containerResolver; + + makeWidget(BuildContext context, ChartContainer chartContainer, + Set desiredGestures) { + _containerResolver = () { + final renderObject = context.findRenderObject()!; + + return getChartContainerRenderObject(renderObject as RenderBox); + }; + + final wantTapDown = desiredGestures.isNotEmpty; + final wantTap = desiredGestures.contains(GestureType.onTap); + final wantDrag = desiredGestures.contains(GestureType.onDrag); + + // LongPress is special, we'd like to be able to trigger long press before + // Drag/Press to trigger tooltips then explore with them. This means we + // can't rely on gesture detection since it will block out the scale + // gestures. + _listeningForLongPress = desiredGestures.contains(GestureType.onLongPress); + + return new GestureDetector( + child: chartContainer, + onTapDown: wantTapDown ? onTapDown : null, + onTapUp: wantTap ? onTapUp : null, + onScaleStart: wantDrag ? onScaleStart : null, + onScaleUpdate: wantDrag ? onScaleUpdate : null, + onScaleEnd: wantDrag ? onScaleEnd : null, + ); + } + + void onTapDown(TapDownDetails d) { + final container = _containerResolver(); + final localPosition = container.globalToLocal(d.globalPosition); + _lastTapPoint = new Point(localPosition.dx, localPosition.dy); + container.gestureProxy.onTapTest(_lastTapPoint!); + + // Kick off a timer to see if this is a LongPress. + if (_listeningForLongPress) { + _longPressTimer = new Timer(_kLongPressTimeout, () { + onLongPress(); + _longPressTimer = null; + }); + } + } + + void onTapUp(TapUpDetails d) { + _longPressTimer?.cancel(); + + final container = _containerResolver(); + final localPosition = container.globalToLocal(d.globalPosition); + _lastTapPoint = new Point(localPosition.dx, localPosition.dy); + container.gestureProxy.onTap(_lastTapPoint!); + } + + void onLongPress() { + final container = _containerResolver(); + container.gestureProxy.onLongPress(_lastTapPoint!); + } + + void onScaleStart(ScaleStartDetails d) { + _longPressTimer?.cancel(); + + final container = _containerResolver(); + final localPosition = container.globalToLocal(d.focalPoint); + _lastTapPoint = new Point(localPosition.dx, localPosition.dy); + + _isDragging = container.gestureProxy.onDragStart(_lastTapPoint!); + } + + void onScaleUpdate(ScaleUpdateDetails d) { + if (!_isDragging) { + return; + } + + final container = _containerResolver(); + final localPosition = container.globalToLocal(d.focalPoint); + _lastTapPoint = new Point(localPosition.dx, localPosition.dy); + _lastScale = d.scale; + + container.gestureProxy.onDragUpdate(_lastTapPoint!, d.scale); + } + + void onScaleEnd(ScaleEndDetails d) { + if (!_isDragging) { + return; + } + + final container = _containerResolver(); + + container.gestureProxy + .onDragEnd(_lastTapPoint!, _lastScale!, d.velocity.pixelsPerSecond.dx); + } +} + +// Exposed for testing. +typedef ChartContainerRenderObject _ContainerResolver(); diff --git a/lib/src/chart_state.dart b/lib/src/chart_state.dart new file mode 100644 index 0000000..6103968 --- /dev/null +++ b/lib/src/chart_state.dart @@ -0,0 +1,36 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +abstract class ChartState { + void setAnimation(Duration transition); + + /// Request to the native platform to rebuild the chart. + void requestRebuild(); + + /// Informs the chart that the configuration has changed. + /// + /// This flag is set by checks that detect if a configuration has changed, + /// such as behaviors, axis, and renderers. + /// + /// This flag is read on chart rebuild, if chart is marked as dirty, then the + /// chart will call a base chart draw. + void markChartDirty(); + + /// Reset the chart dirty flag. + void resetChartDirtyFlag(); + + /// Gets if the chart is dirty. + bool get chartIsDirty; +} diff --git a/lib/src/combo_chart/combo_chart.dart b/lib/src/combo_chart/combo_chart.dart new file mode 100644 index 0000000..e4d30fc --- /dev/null +++ b/lib/src/combo_chart/combo_chart.dart @@ -0,0 +1,123 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + NumericCartesianChart, + OrdinalCartesianChart, + NumericAxisSpec, + RTLSpec, + Series, + SeriesRendererConfig; +import '../behaviors/chart_behavior.dart' show ChartBehavior; +import '../base_chart.dart' show LayoutConfig; +import '../base_chart_state.dart' show BaseChartState; +import '../cartesian_chart.dart' show CartesianChart; +import '../selection_model_config.dart' show SelectionModelConfig; + +/// A numeric combo chart supports rendering each series of data with different +/// series renderers. +/// +/// Note that if you have DateTime data, you should use [TimeSeriesChart]. We do +/// not expose a separate DateTimeComboChart because it would just be a copy of +/// that chart. +class NumericComboChart extends CartesianChart { + NumericComboChart( + List> seriesList, { + bool? animate, + Duration? animationDuration, + common.AxisSpec? domainAxis, + common.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? secondaryMeasureAxis, + common.SeriesRendererConfig? defaultRenderer, + List>? customSeriesRenderers, + List>? behaviors, + List>? selectionModels, + common.RTLSpec? rtlSpec, + LayoutConfig? layoutConfig, + bool defaultInteractions = true, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + ); + + @override + common.NumericCartesianChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.NumericCartesianChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis()); + } +} + +/// An ordinal combo chart supports rendering each series of data with different +/// series renderers. +class OrdinalComboChart extends CartesianChart { + OrdinalComboChart( + List> seriesList, { + bool? animate, + Duration? animationDuration, + common.AxisSpec? domainAxis, + common.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? secondaryMeasureAxis, + common.SeriesRendererConfig? defaultRenderer, + List>? customSeriesRenderers, + List>? behaviors, + List>? selectionModels, + common.RTLSpec? rtlSpec, + LayoutConfig? layoutConfig, + bool defaultInteractions = true, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + ); + + @override + common.OrdinalCartesianChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.OrdinalCartesianChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis()); + } +} diff --git a/lib/src/graphics_factory.dart b/lib/src/graphics_factory.dart new file mode 100644 index 0000000..9a76729 --- /dev/null +++ b/lib/src/graphics_factory.dart @@ -0,0 +1,55 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show GraphicsFactory, LineStyle, TextElement, TextStyle; +import 'package:flutter/widgets.dart' + show BuildContext, DefaultTextStyle, MediaQuery; +import 'line_style.dart' show LineStyle; +import 'text_element.dart' show TextElement; +import 'text_style.dart' show TextStyle; + +class GraphicsFactory implements common.GraphicsFactory { + final double textScaleFactor; + final DefaultTextStyle defaultTextStyle; + + GraphicsFactory(BuildContext context, + {GraphicsFactoryHelper helper = const GraphicsFactoryHelper()}) + : textScaleFactor = helper.getTextScaleFactorOf(context), + defaultTextStyle = DefaultTextStyle.of(context); + + /// Returns a [TextStyle] object. + @override + common.TextStyle createTextPaint() => + TextStyle()..fontFamily = defaultTextStyle.style.fontFamily; + + /// Returns a text element from [text]. + @override + common.TextElement createTextElement(String text) { + return TextElement(text, textScaleFactor: textScaleFactor) + ..textStyle = createTextPaint(); + } + + @override + common.LineStyle createLinePaint() => LineStyle(); +} + +/// Wraps the MediaQuery function to allow for testing. +class GraphicsFactoryHelper { + const GraphicsFactoryHelper(); + + double getTextScaleFactorOf(BuildContext context) => + MediaQuery.textScaleFactorOf(context); +} diff --git a/lib/src/line_chart.dart b/lib/src/line_chart.dart new file mode 100644 index 0000000..8e71b61 --- /dev/null +++ b/lib/src/line_chart.dart @@ -0,0 +1,90 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection' show LinkedHashMap; + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + LineChart, + NumericAxisSpec, + RTLSpec, + Series, + LineRendererConfig, + SeriesRendererConfig; +import 'behaviors/line_point_highlighter.dart' show LinePointHighlighter; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'base_chart.dart' show LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'cartesian_chart.dart' show CartesianChart; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +class LineChart extends CartesianChart { + LineChart( + List> seriesList, { + bool? animate, + Duration? animationDuration, + common.AxisSpec? domainAxis, + common.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? secondaryMeasureAxis, + LinkedHashMap? disjointMeasureAxes, + common.LineRendererConfig? defaultRenderer, + List>? customSeriesRenderers, + List>? behaviors, + List>? selectionModels, + common.RTLSpec? rtlSpec, + LayoutConfig? layoutConfig, + bool defaultInteractions = true, + bool? flipVerticalAxis, + UserManagedState? userManagedState, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + flipVerticalAxis: flipVerticalAxis, + userManagedState: userManagedState, + ); + + @override + common.LineChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.LineChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(), + disjointMeasureAxes: createDisjointMeasureAxes()); + } + + @override + void addDefaultInteractions(List behaviors) { + super.addDefaultInteractions(behaviors); + + behaviors.add(new LinePointHighlighter()); + } +} diff --git a/lib/src/line_style.dart b/lib/src/line_style.dart new file mode 100644 index 0000000..6a7d0e1 --- /dev/null +++ b/lib/src/line_style.dart @@ -0,0 +1,29 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common show Color, LineStyle; + +class LineStyle implements common.LineStyle { + @override + common.Color? color; + + @override + List? dashPattern; + + @override + int strokeWidth; + + LineStyle({this.strokeWidth = 0}); +} diff --git a/lib/src/pie_chart.dart b/lib/src/pie_chart.dart new file mode 100644 index 0000000..48a8e16 --- /dev/null +++ b/lib/src/pie_chart.dart @@ -0,0 +1,49 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show ArcRendererConfig, PieChart, RTLSpec, Series; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'base_chart.dart' show BaseChart, LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'selection_model_config.dart' show SelectionModelConfig; + +class PieChart extends BaseChart { + PieChart( + List> seriesList, { + bool? animate, + Duration? animationDuration, + common.ArcRendererConfig? defaultRenderer, + List>? behaviors, + List>? selectionModels, + common.RTLSpec? rtlSpec, + LayoutConfig? layoutConfig, + bool defaultInteractions = true, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + defaultRenderer: defaultRenderer, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + ); + + @override + common.PieChart createCommonChart(BaseChartState chartState) => + new common.PieChart(layoutConfig: layoutConfig?.commonLayoutConfig); +} diff --git a/lib/src/scatter_plot_chart.dart b/lib/src/scatter_plot_chart.dart new file mode 100644 index 0000000..c353410 --- /dev/null +++ b/lib/src/scatter_plot_chart.dart @@ -0,0 +1,82 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection' show LinkedHashMap; + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + NumericAxisSpec, + PointRendererConfig, + RTLSpec, + ScatterPlotChart, + SeriesRendererConfig, + Series; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'base_chart.dart' show LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'cartesian_chart.dart' show CartesianChart; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +class ScatterPlotChart extends CartesianChart { + ScatterPlotChart( + List> seriesList, { + bool? animate, + Duration? animationDuration, + common.AxisSpec? domainAxis, + common.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? secondaryMeasureAxis, + LinkedHashMap? disjointMeasureAxes, + common.PointRendererConfig? defaultRenderer, + List>? customSeriesRenderers, + List>? behaviors, + List>? selectionModels, + common.RTLSpec? rtlSpec, + LayoutConfig? layoutConfig, + bool defaultInteractions = true, + bool? flipVerticalAxis, + UserManagedState? userManagedState, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + rtlSpec: rtlSpec, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + flipVerticalAxis: flipVerticalAxis, + userManagedState: userManagedState, + ); + + @override + common.ScatterPlotChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.ScatterPlotChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(), + disjointMeasureAxes: createDisjointMeasureAxes()); + } +} diff --git a/lib/src/selection_model_config.dart b/lib/src/selection_model_config.dart new file mode 100644 index 0000000..60305a4 --- /dev/null +++ b/lib/src/selection_model_config.dart @@ -0,0 +1,34 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:meta/meta.dart' show immutable; + +import 'package:charts_common/common.dart' as common; + +@immutable +class SelectionModelConfig { + final common.SelectionModelType type; + + /// Listens for change in selection. + final common.SelectionModelListener? changedListener; + + /// Listens anytime update selection is called. + final common.SelectionModelListener? updatedListener; + + SelectionModelConfig( + {this.type = common.SelectionModelType.info, + this.changedListener, + this.updatedListener}); +} diff --git a/lib/src/symbol_renderer.dart b/lib/src/symbol_renderer.dart new file mode 100644 index 0000000..dd26d6f --- /dev/null +++ b/lib/src/symbol_renderer.dart @@ -0,0 +1,116 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Rectangle; +import 'package:charts_common/common.dart' as common + show ChartCanvas, Color, FillPatternType, SymbolRenderer; +import 'package:flutter/widgets.dart'; +import 'chart_canvas.dart' show ChartCanvas; +import 'graphics_factory.dart' show GraphicsFactory; + +/// Flutter widget responsible for painting a common SymbolRenderer from the +/// chart. +/// +/// If you want to customize the symbol, then use [CustomSymbolRenderer]. +class SymbolRendererCanvas implements SymbolRendererBuilder { + final common.SymbolRenderer commonSymbolRenderer; + final List? dashPattern; + + SymbolRendererCanvas(this.commonSymbolRenderer, this.dashPattern); + + @override + Widget build(BuildContext context, + {Color? color, required Size size, bool enabled = true}) { + if (color != null && !enabled) { + color = color.withOpacity(0.26); + } + + return new SizedBox.fromSize( + size: size, + child: new CustomPaint( + painter: new _SymbolCustomPaint( + context, commonSymbolRenderer, color, dashPattern))); + } +} + +/// Convenience class allowing you to pass your Widget builder through the +/// common chart so that it is created for you by the Legend. +/// +/// This allows a custom SymbolRenderer in Flutter without having to create +/// a completely custom legend. +abstract class CustomSymbolRenderer extends common.SymbolRenderer + implements SymbolRendererBuilder { + CustomSymbolRenderer() : super(isSolid: false); + + /// Must override this method to build the custom Widget with the given color + /// as + @override + Widget build(BuildContext context, + {Color? color, required Size size, bool enabled = true}); + + @override + void paint(common.ChartCanvas canvas, Rectangle bounds, + {List? dashPattern, + common.Color? fillColor, + common.FillPatternType? fillPattern, + common.Color? strokeColor, + double? strokeWidthPx}) { + // Intentionally ignored (never called). + } + + @override + bool shouldRepaint(common.SymbolRenderer oldRenderer) { + return false; // Repainting is handled directly in Flutter. + } +} + +/// Common interface for [CustomSymbolRenderer] & [SymbolRendererCanvas] for +/// convenience for [LegendEntryLayout]. +abstract class SymbolRendererBuilder { + Widget build(BuildContext context, + {Color? color, required Size size, bool enabled}); +} + +/// The Widget which fulfills the guts of [SymbolRendererCanvas] actually +/// painting the symbol to a canvas using [CustomPainter]. +class _SymbolCustomPaint extends CustomPainter { + final BuildContext context; + final common.SymbolRenderer symbolRenderer; + final Color? color; + final List? dashPattern; + + _SymbolCustomPaint( + this.context, this.symbolRenderer, this.color, this.dashPattern); + + @override + void paint(Canvas canvas, Size size) { + final bounds = + new Rectangle(0, 0, size.width.toInt(), size.height.toInt()); + final commonColor = color == null + ? null + : new common.Color( + r: color!.red, g: color!.green, b: color!.blue, a: color!.alpha); + symbolRenderer.paint( + new ChartCanvas(canvas, GraphicsFactory(context)), bounds, + fillColor: commonColor, + strokeColor: commonColor, + dashPattern: dashPattern); + } + + @override + bool shouldRepaint(_SymbolCustomPaint oldDelegate) { + return symbolRenderer.shouldRepaint(oldDelegate.symbolRenderer); + } +} diff --git a/lib/src/text_element.dart b/lib/src/text_element.dart new file mode 100644 index 0000000..b2c37f1 --- /dev/null +++ b/lib/src/text_element.dart @@ -0,0 +1,186 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:ui' show TextAlign, TextDirection; +import 'package:charts_common/common.dart' as common + show + MaxWidthStrategy, + TextElement, + TextDirection, + TextMeasurement, + TextStyle; +import 'package:flutter/rendering.dart' + show Color, TextBaseline, TextPainter, TextSpan, TextStyle; + +/// Flutter implementation for text measurement and painter. +class TextElement implements common.TextElement { + static const ellipsis = '\u{2026}'; + + @override + final String text; + + final double? textScaleFactor; + + var _painterReady = false; + common.TextStyle? _textStyle; + common.TextDirection _textDirection = common.TextDirection.ltr; + + int? _maxWidth; + common.MaxWidthStrategy? _maxWidthStrategy; + + late TextPainter _textPainter; + + late common.TextMeasurement _measurement; + + double? _opacity; + + TextElement(this.text, {common.TextStyle? style, this.textScaleFactor}) + : _textStyle = style; + + @override + common.TextStyle? get textStyle => _textStyle; + + @override + set textStyle(common.TextStyle? value) { + if (_textStyle == value) { + return; + } + _textStyle = value; + _painterReady = false; + } + + @override + set textDirection(common.TextDirection direction) { + if (_textDirection == direction) { + return; + } + _textDirection = direction; + _painterReady = false; + } + + @override + common.TextDirection get textDirection => _textDirection; + + @override + int? get maxWidth => _maxWidth; + + @override + set maxWidth(int? value) { + if (_maxWidth == value) { + return; + } + _maxWidth = value; + _painterReady = false; + } + + @override + common.MaxWidthStrategy? get maxWidthStrategy => _maxWidthStrategy; + + @override + set maxWidthStrategy(common.MaxWidthStrategy? maxWidthStrategy) { + if (_maxWidthStrategy == maxWidthStrategy) { + return; + } + _maxWidthStrategy = maxWidthStrategy; + _painterReady = false; + } + + @override + set opacity(double? opacity) { + if (opacity != _opacity) { + _painterReady = false; + _opacity = opacity; + } + } + + @override + common.TextMeasurement get measurement { + if (!_painterReady) { + _refreshPainter(); + } + + return _measurement; + } + + /// The estimated distance between where we asked to draw the text (top, left) + /// and where it visually started (top + verticalFontShift, left). + /// + /// 10% of reported font height seems to be about right. + int get verticalFontShift { + if (!_painterReady) { + _refreshPainter(); + } + + return (_textPainter.height * 0.1).ceil(); + } + + TextPainter? get textPainter { + if (!_painterReady) { + _refreshPainter(); + } + return _textPainter; + } + + /// Create text painter and measure based on current settings + void _refreshPainter() { + _opacity ??= 1.0; + var color = (textStyle == null || textStyle!.color == null) + ? null + : new Color.fromARGB( + (textStyle!.color!.a * _opacity!).round(), + textStyle!.color!.r, + textStyle!.color!.g, + textStyle!.color!.b, + ); + + _textPainter = new TextPainter( + text: new TextSpan( + text: text, + style: new TextStyle( + color: color, + fontSize: textStyle?.fontSize?.toDouble(), + fontFamily: textStyle?.fontFamily, + height: textStyle?.lineHeight))) + ..textDirection = TextDirection.ltr + // TODO Flip once textAlign works + ..textAlign = TextAlign.left + // ..textAlign = _textDirection == common.TextDirection.rtl ? + // TextAlign.right : TextAlign.left + ..ellipsis = maxWidthStrategy == common.MaxWidthStrategy.ellipsize + ? ellipsis + : null; + + if (textScaleFactor != null) { + _textPainter.textScaleFactor = textScaleFactor!; + } + + _textPainter.layout(maxWidth: maxWidth?.toDouble() ?? double.infinity); + + final baseline = + _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic); + + // Estimating the actual draw height to 70% of measures size. + // + // The font reports a size larger than the drawn size, which makes it + // difficult to shift the text around to get it to visually line up + // vertically with other components. + _measurement = new common.TextMeasurement( + horizontalSliceWidth: _textPainter.width, + verticalSliceWidth: _textPainter.height * 0.70, + baseline: baseline); + + _painterReady = true; + } +} diff --git a/lib/src/text_style.dart b/lib/src/text_style.dart new file mode 100644 index 0000000..ef81d7f --- /dev/null +++ b/lib/src/text_style.dart @@ -0,0 +1,38 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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:ui' ; +import 'package:charts_common/common.dart' as common show Color, TextStyle; + +class TextStyle implements common.TextStyle { + int? fontSize; + String? fontFamily; + common.Color? color; + double? lineHeight; + String? fontWeight; + + @override + bool operator ==(Object other) => + other is TextStyle && + fontSize == other.fontSize && + fontFamily == other.fontFamily && + fontWeight == other.fontWeight && + color == other.color && + lineHeight == other.lineHeight; + + @override + int get hashCode => + Object.hash(fontSize, fontFamily, color, lineHeight, fontWeight); +} diff --git a/lib/src/time_series_chart.dart b/lib/src/time_series_chart.dart new file mode 100644 index 0000000..13f7e4f --- /dev/null +++ b/lib/src/time_series_chart.dart @@ -0,0 +1,97 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:collection' show LinkedHashMap; + +import 'package:charts_common/common.dart' as common + show + AxisSpec, + DateTimeFactory, + LocalDateTimeFactory, + NumericAxisSpec, + Series, + SeriesRendererConfig, + TimeSeriesChart; +import 'behaviors/chart_behavior.dart' show ChartBehavior; +import 'behaviors/line_point_highlighter.dart' show LinePointHighlighter; +import 'cartesian_chart.dart' show CartesianChart; +import 'base_chart.dart' show LayoutConfig; +import 'base_chart_state.dart' show BaseChartState; +import 'selection_model_config.dart' show SelectionModelConfig; +import 'user_managed_state.dart' show UserManagedState; + +class TimeSeriesChart extends CartesianChart { + final common.DateTimeFactory? dateTimeFactory; + + /// Create a [TimeSeriesChart]. + /// + /// [dateTimeFactory] allows specifying a factory that creates [DateTime] to + /// be used for the time axis. If none specified, local date time is used. + TimeSeriesChart( + List> seriesList, { + bool? animate, + Duration? animationDuration, + common.AxisSpec? domainAxis, + common.NumericAxisSpec? primaryMeasureAxis, + common.NumericAxisSpec? secondaryMeasureAxis, + LinkedHashMap? disjointMeasureAxes, + common.SeriesRendererConfig? defaultRenderer, + List>? customSeriesRenderers, + List>? behaviors, + List>? selectionModels, + LayoutConfig? layoutConfig, + this.dateTimeFactory, + bool defaultInteractions = true, + bool? flipVerticalAxis, + UserManagedState? userManagedState, + }) : super( + seriesList, + animate: animate, + animationDuration: animationDuration, + domainAxis: domainAxis, + primaryMeasureAxis: primaryMeasureAxis, + secondaryMeasureAxis: secondaryMeasureAxis, + disjointMeasureAxes: disjointMeasureAxes, + defaultRenderer: defaultRenderer, + customSeriesRenderers: customSeriesRenderers, + behaviors: behaviors, + selectionModels: selectionModels, + layoutConfig: layoutConfig, + defaultInteractions: defaultInteractions, + flipVerticalAxis: flipVerticalAxis, + userManagedState: userManagedState, + ); + + @override + common.TimeSeriesChart createCommonChart(BaseChartState chartState) { + // Optionally create primary and secondary measure axes if the chart was + // configured with them. If no axes were configured, then the chart will + // use its default types (usually a numeric axis). + return new common.TimeSeriesChart( + layoutConfig: layoutConfig?.commonLayoutConfig, + primaryMeasureAxis: primaryMeasureAxis?.createAxis(), + secondaryMeasureAxis: secondaryMeasureAxis?.createAxis(), + disjointMeasureAxes: createDisjointMeasureAxes(), + dateTimeFactory: + dateTimeFactory ?? const common.LocalDateTimeFactory()); + } + + @override + void addDefaultInteractions(List behaviors) { + super.addDefaultInteractions(behaviors); + + behaviors.add(new LinePointHighlighter()); + } +} diff --git a/lib/src/user_managed_state.dart b/lib/src/user_managed_state.dart new file mode 100644 index 0000000..8b32536 --- /dev/null +++ b/lib/src/user_managed_state.dart @@ -0,0 +1,77 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common + show ImmutableSeries, SelectionModel, SelectionModelType, SeriesDatumConfig; + +/// Contains override settings for the internal chart state. +/// +/// The chart will check non null settings and apply them if they differ from +/// the internal chart state and trigger the appropriate level of redrawing. +class UserManagedState { + /// The expected selection(s) on the chart. + /// + /// If this is set and the model for the selection model type differs from + /// what is in the internal chart state, the selection will be applied and + /// repainting will occur such that behaviors that draw differently on + /// selection change can update, such as the line point highlighter. + /// + /// If more than one type of selection model is used, only the one(s) + /// specified in this list will override what is kept in the internally. + /// + /// To clear the selection, add an empty selection model. + final selectionModels = + >{}; +} + +/// Container for the user managed selection model. +/// +/// This container is needed because the selection model generated by selection +/// events is a [SelectionModel], while any user defined selection has to be +/// specified by passing in [selectedSeriesConfig] and [selectedDataConfig]. +/// The configuration is converted to a selection model after the series data +/// has been processed. +class UserManagedSelectionModel { + final List? selectedSeriesConfig; + final List>? selectedDataConfig; + common.SelectionModel? _model; + + /// Creates a [UserManagedSelectionModel] that holds [SelectionModel]. + /// + /// [selectedSeriesConfig] and [selectedDataConfig] is set to null because the + /// [_model] is returned when [getModel] is called. + UserManagedSelectionModel({common.SelectionModel? model}) + : _model = model ?? common.SelectionModel(), + selectedSeriesConfig = null, + selectedDataConfig = null; + + /// Creates a [UserManagedSelectionModel] with configuration that is converted + /// to a [SelectionModel] when [getModel] provides a processed series list. + UserManagedSelectionModel.fromConfig( + {List? selectedSeriesConfig, + List>? selectedDataConfig}) + : this.selectedSeriesConfig = selectedSeriesConfig, + this.selectedDataConfig = selectedDataConfig; + + /// Gets the selection model. If the model is null, create one from + /// configuration and the processed [seriesList] passed in. + common.SelectionModel getModel( + List> seriesList) { + _model ??= common.SelectionModel.fromConfig( + selectedDataConfig, selectedSeriesConfig, seriesList); + + return _model!; + } +} diff --git a/lib/src/util.dart b/lib/src/util.dart new file mode 100644 index 0000000..b391748 --- /dev/null +++ b/lib/src/util.dart @@ -0,0 +1,45 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/rendering.dart' + show + RenderBox, + RenderSemanticsGestureHandler, + RenderPointerListener, + RenderCustomMultiChildLayoutBox; +import 'chart_container.dart' show ChartContainerRenderObject; + +/// Get the [ChartContainerRenderObject] from a [RenderBox]. +/// +/// [RenderBox] is expected to be a [RenderSemanticsGestureHandler] with child +/// of [RenderPointerListener] with child of [ChartContainerRenderObject]. +ChartContainerRenderObject getChartContainerRenderObject(RenderBox box) { + assert(box is RenderCustomMultiChildLayoutBox); + final semanticHandler = (box as RenderCustomMultiChildLayoutBox) + .getChildrenAsList() + .firstWhere((child) => child is RenderSemanticsGestureHandler); + + assert(semanticHandler is RenderSemanticsGestureHandler); + final renderPointerListener = + (semanticHandler as RenderSemanticsGestureHandler).child; + + assert(renderPointerListener is RenderPointerListener); + final chartContainerRenderObject = + (renderPointerListener as RenderPointerListener).child; + + assert(chartContainerRenderObject is ChartContainerRenderObject); + + return chartContainerRenderObject as ChartContainerRenderObject; +} diff --git a/lib/src/util/color.dart b/lib/src/util/color.dart new file mode 100644 index 0000000..e4297a3 --- /dev/null +++ b/lib/src/util/color.dart @@ -0,0 +1,28 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:charts_common/common.dart' as common show Color; +import 'dart:ui' as ui; + +class ColorUtil { + static ui.Color toDartColor(common.Color color) { + return ui.Color.fromARGB(color.a, color.r, color.g, color.b); + } + + static common.Color fromDartColor(ui.Color color) { + return common.Color( + r: color.red, g: color.green, b: color.blue, a: color.alpha); + } +} diff --git a/lib/src/widget_layout_delegate.dart b/lib/src/widget_layout_delegate.dart new file mode 100644 index 0000000..b9288c5 --- /dev/null +++ b/lib/src/widget_layout_delegate.dart @@ -0,0 +1,222 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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:ui' show Offset; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:charts_common/common.dart' as common + show BehaviorPosition, InsideJustification, OutsideJustification; + +import 'behaviors/chart_behavior.dart' show BuildableBehavior; + +/// Layout delegate that layout chart widget with [BuildableBehavior] widgets. +class WidgetLayoutDelegate extends MultiChildLayoutDelegate { + /// ID of the common chart widget. + final String chartID; + + /// Directionality of the widget. + final isRTL; + + /// ID and [BuildableBehavior] of the widgets for calculating offset. + final Map idAndBehavior; + + WidgetLayoutDelegate(this.chartID, this.idAndBehavior, this.isRTL); + + @override + void performLayout(Size size) { + // TODO: Change this to a layout manager that supports more + // than one buildable behavior that changes chart size. Remove assert when + // this is possible. + assert(idAndBehavior.keys.isEmpty || idAndBehavior.keys.length == 1); + + // Size available for the chart widget. + var availableWidth = size.width; + var availableHeight = size.height; + var chartOffset = Offset.zero; + + // Measure the first buildable behavior. + final behaviorID = + idAndBehavior.keys.isNotEmpty ? idAndBehavior.keys.first : null; + var behaviorSize = Size.zero; + if (behaviorID != null) { + if (hasChild(behaviorID)) { + final leftPosition = + isRTL ? common.BehaviorPosition.end : common.BehaviorPosition.start; + final rightPosition = + isRTL ? common.BehaviorPosition.start : common.BehaviorPosition.end; + final behaviorPosition = idAndBehavior[behaviorID]!.position; + + behaviorSize = layoutChild(behaviorID, new BoxConstraints.loose(size)); + if (behaviorPosition == common.BehaviorPosition.top) { + chartOffset = new Offset(0.0, behaviorSize.height); + availableHeight -= behaviorSize.height; + } else if (behaviorPosition == common.BehaviorPosition.bottom) { + availableHeight -= behaviorSize.height; + } else if (behaviorPosition == leftPosition) { + chartOffset = new Offset(behaviorSize.width, 0.0); + availableWidth -= behaviorSize.width; + } else if (behaviorPosition == rightPosition) { + availableWidth -= behaviorSize.width; + } + } + } + + // Layout chart. + final chartSize = new Size(availableWidth, availableHeight); + if (hasChild(chartID)) { + layoutChild(chartID, new BoxConstraints.tight(chartSize)); + positionChild(chartID, chartOffset); + } + + // Position buildable behavior. + if (behaviorID != null) { + // TODO: Unable to relayout with new smaller width. + // In the delegate, all children are required to have layout called + // exactly once. + final behaviorOffset = _getBehaviorOffset(idAndBehavior[behaviorID]!, + behaviorSize: behaviorSize, chartSize: chartSize, isRTL: isRTL); + + positionChild(behaviorID, behaviorOffset); + } + } + + @override + bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) { + // TODO: Deep equality check because the instance will not be + // the same on each build, even if the buildable behavior has not changed. + return idAndBehavior != (oldDelegate as WidgetLayoutDelegate).idAndBehavior; + } + + // Calculate buildable behavior's offset. + Offset _getBehaviorOffset(BuildableBehavior behavior, + {required Size behaviorSize, + required Size chartSize, + required bool isRTL}) { + late Offset behaviorOffset; + + final behaviorPosition = behavior.position; + final outsideJustification = behavior.outsideJustification; + final insideJustification = behavior.insideJustification; + + if (behaviorPosition == common.BehaviorPosition.top || + behaviorPosition == common.BehaviorPosition.bottom) { + final heightOffset = behaviorPosition == common.BehaviorPosition.bottom + ? chartSize.height + : 0.0; + + final horizontalJustification = + getOutsideJustification(outsideJustification, isRTL); + + switch (horizontalJustification) { + case _HorizontalJustification.leftDrawArea: + behaviorOffset = new Offset( + behavior.drawAreaBounds!.left.toDouble(), heightOffset); + break; + case _HorizontalJustification.left: + behaviorOffset = new Offset(0.0, heightOffset); + break; + case _HorizontalJustification.rightDrawArea: + behaviorOffset = new Offset( + behavior.drawAreaBounds!.right - behaviorSize.width, + heightOffset); + break; + case _HorizontalJustification.right: + behaviorOffset = + new Offset(chartSize.width - behaviorSize.width, heightOffset); + break; + } + } else if (behaviorPosition == common.BehaviorPosition.start || + behaviorPosition == common.BehaviorPosition.end) { + final widthOffset = + (isRTL && behaviorPosition == common.BehaviorPosition.start) || + (!isRTL && behaviorPosition == common.BehaviorPosition.end) + ? chartSize.width + : 0.0; + + switch (outsideJustification) { + case common.OutsideJustification.startDrawArea: + case common.OutsideJustification.middleDrawArea: + behaviorOffset = + new Offset(widthOffset, behavior.drawAreaBounds!.top.toDouble()); + break; + case common.OutsideJustification.start: + case common.OutsideJustification.middle: + behaviorOffset = new Offset(widthOffset, 0.0); + break; + case common.OutsideJustification.endDrawArea: + behaviorOffset = new Offset(widthOffset, + behavior.drawAreaBounds!.bottom - behaviorSize.height); + break; + case common.OutsideJustification.end: + behaviorOffset = + new Offset(widthOffset, chartSize.height - behaviorSize.height); + break; + } + } else if (behaviorPosition == common.BehaviorPosition.inside) { + var rightOffset = new Offset(chartSize.width - behaviorSize.width, 0.0); + + switch (insideJustification) { + case common.InsideJustification.topStart: + behaviorOffset = isRTL ? rightOffset : Offset.zero; + break; + case common.InsideJustification.topEnd: + behaviorOffset = isRTL ? Offset.zero : rightOffset; + break; + } + } + + return behaviorOffset; + } + + _HorizontalJustification getOutsideJustification( + common.OutsideJustification justification, bool isRTL) { + _HorizontalJustification mappedJustification; + + switch (justification) { + case common.OutsideJustification.startDrawArea: + case common.OutsideJustification.middleDrawArea: + mappedJustification = isRTL + ? _HorizontalJustification.rightDrawArea + : _HorizontalJustification.leftDrawArea; + break; + case common.OutsideJustification.start: + case common.OutsideJustification.middle: + mappedJustification = isRTL + ? _HorizontalJustification.right + : _HorizontalJustification.left; + break; + case common.OutsideJustification.endDrawArea: + mappedJustification = isRTL + ? _HorizontalJustification.leftDrawArea + : _HorizontalJustification.rightDrawArea; + break; + case common.OutsideJustification.end: + mappedJustification = isRTL + ? _HorizontalJustification.left + : _HorizontalJustification.right; + break; + } + + return mappedJustification; + } +} + +enum _HorizontalJustification { + leftDrawArea, + left, + rightDrawArea, + right, +} diff --git a/minimum_os.bzl b/minimum_os.bzl new file mode 100644 index 0000000..964ed7a --- /dev/null +++ b/minimum_os.bzl @@ -0,0 +1,3 @@ +"""Minimum OS version definitions""" + +IOS_MINIMUM_OS = "9.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..425b5dd --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,33 @@ +name: charts_flutter +version: 0.12.0 +description: Material Design charting library for flutter. +homepage: https://github.com/google/charts + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: '>=2.0.0' + # flutter: '>=2.5.0' + +dependencies: + # Pointing this to a local path allows for pointing to the latest code + # in Github for open source development. + # + # The pub version of charts_flutter will point to the pub version of charts_common. + # The latest pub version is commented out and shown below as an example. + charts_common: ^0.12.0 + # charts_common: + # path: ../charts_common/ + collection: ^1.14.5 + flutter: + sdk: flutter + intl: ">=0.15.2 < 0.18.0" + logging: ^1.0.2 + meta: ^1.1.1 + + +dev_dependencies: + mockito: ^5.0.0 + build_runner: ^1.11.0 + flutter_test: + sdk: flutter + test: ^1.3.0 diff --git a/test/behaviors/legend/legend_layout_test.dart b/test/behaviors/legend/legend_layout_test.dart new file mode 100644 index 0000000..0165914 --- /dev/null +++ b/test/behaviors/legend/legend_layout_test.dart @@ -0,0 +1,117 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'package:charts_flutter/src/behaviors/legend/legend_layout.dart'; + +// Can't use Mockito annotations with BuildContext yet? Fake it. +class FakeBuildContext extends Fake implements BuildContext {} + +void main() { + late BuildContext context; + + setUp(() { + context = FakeBuildContext(); + }); + + group('TabularLegendLayoutBuilder', () { + test('builds horizontally', () { + final builder = new TabularLegendLayout.horizontalFirst(); + final widgets = [new Text('1'), new Text('2'), new Text('3')]; + + final Table layout = builder.build(context, widgets) as Table; + expect(layout.children.length, 1); + expect(layout.children.first.children!.length, 3); + }); + + test('does not build extra columns if max columns exceed widget count', () { + final builder = + new TabularLegendLayout.horizontalFirst(desiredMaxColumns: 10); + final widgets = [new Text('1'), new Text('2'), new Text('3')]; + + final Table layout = builder.build(context, widgets) as Table; + expect(layout.children.length, 1); + expect(layout.children.first.children!.length, 3); + }); + + test('builds horizontally until max column exceeded', () { + final builder = + new TabularLegendLayout.horizontalFirst(desiredMaxColumns: 2); + + final widgets = new List.generate( + 7, (int index) => new Text(index.toString())); + + final Table layout = builder.build(context, widgets) as Table; + expect(layout.children.length, 4); + + expect(layout.children[0].children![0], equals(widgets[0])); + expect(layout.children[0].children![1], equals(widgets[1])); + + expect(layout.children[1].children![0], equals(widgets[2])); + expect(layout.children[1].children![1], equals(widgets[3])); + + expect(layout.children[2].children![0], equals(widgets[4])); + expect(layout.children[2].children![1], equals(widgets[5])); + + expect(layout.children[3].children![0], equals(widgets[6])); + }); + + test('builds vertically', () { + final builder = new TabularLegendLayout.verticalFirst(); + final widgets = [new Text('1'), new Text('2'), new Text('3')]; + + final Table layout = builder.build(context, widgets) as Table; + expect(layout.children.length, 3); + expect(layout.children[0].children!.length, 1); + expect(layout.children[1].children!.length, 1); + expect(layout.children[2].children!.length, 1); + }); + + test('does not build extra rows if max rows exceed widget count', () { + final builder = new TabularLegendLayout.verticalFirst(desiredMaxRows: 10); + final widgets = [new Text('1'), new Text('2'), new Text('3')]; + + final Table layout = builder.build(context, widgets) as Table; + expect(layout.children.length, 3); + expect(layout.children[0].children!.length, 1); + expect(layout.children[1].children!.length, 1); + expect(layout.children[2].children!.length, 1); + }); + + test('builds vertically until max column exceeded', () { + final builder = new TabularLegendLayout.verticalFirst(desiredMaxRows: 2); + + final widgets = new List.generate( + 7, (int index) => new Text(index.toString())); + + final Table layout = builder.build(context, widgets) as Table; + expect(layout.children.length, 2); + + expect(layout.children[0].children![0], equals(widgets[0])); + expect(layout.children[1].children![0], equals(widgets[1])); + + expect(layout.children[0].children![1], equals(widgets[2])); + expect(layout.children[1].children![1], equals(widgets[3])); + + expect(layout.children[0].children![2], equals(widgets[4])); + expect(layout.children[1].children![2], equals(widgets[5])); + + expect(layout.children[0].children![3], equals(widgets[6])); + }); + }); +} diff --git a/test/text_element_test.dart b/test/text_element_test.dart new file mode 100644 index 0000000..6cdd4cd --- /dev/null +++ b/test/text_element_test.dart @@ -0,0 +1,54 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart' show BuildContext; +import 'package:mockito/mockito.dart'; +import 'package:flutter/widgets.dart' show InheritedWidget; +import 'package:test/test.dart'; +import 'package:charts_flutter/src/graphics_factory.dart'; +import 'package:charts_flutter/src/text_element.dart'; + +// Can't use Mockito annotations with BuildContext yet? Fake it. +class FakeBuildContext extends Fake implements BuildContext { + @override + T? dependOnInheritedWidgetOfExactType( + {Object? aspect}) { + return null; + } +} + +// Gave up trying to figure out how to use mockito for now. +class FakeGraphicsFactoryHelper extends Fake implements GraphicsFactoryHelper { + double textScaleFactor; + + FakeGraphicsFactoryHelper(this.textScaleFactor) {} + + @override + double getTextScaleFactorOf(BuildContext context) => textScaleFactor; +} + +void main() { + test('Text element gets assigned scale factor', () { + final context = FakeBuildContext(); + final helper = FakeGraphicsFactoryHelper(3.0); + final graphicsFactory = new GraphicsFactory(context, helper: helper); + + final textElement = + graphicsFactory.createTextElement('test') as TextElement; + + expect(textElement.text, equals('test')); + expect(textElement.textScaleFactor, equals(3.0)); + }); +} diff --git a/test/user_managed_state_test.dart b/test/user_managed_state_test.dart new file mode 100644 index 0000000..5fb392b --- /dev/null +++ b/test/user_managed_state_test.dart @@ -0,0 +1,137 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:charts_flutter/flutter.dart' as charts; + +void main() { + testWidgets('selection can be set programmatically', + (WidgetTester tester) async { + final onTapSelection = + new charts.UserManagedSelectionModel.fromConfig( + selectedDataConfig: [ + new charts.SeriesDatumConfig('Sales', '2016') + ]); + + charts.SelectionModel? currentSelectionModel; + + void selectionChangedListener(charts.SelectionModel model) { + currentSelectionModel = model; + } + + final testChart = new TestChart(selectionChangedListener, onTapSelection); + + await tester.pumpWidget( + new Directionality( + textDirection: TextDirection.ltr, + child: testChart, + ), + ); + + expect(currentSelectionModel, isNull); + + await tester.tap(find.byType(charts.BarChart)); + + await tester.pump(); + + expect(currentSelectionModel, isNotNull); + expect(currentSelectionModel!.selectedDatum, hasLength(1)); + final selectedDatum = + currentSelectionModel!.selectedDatum.first.datum as OrdinalSales; + expect(selectedDatum.year, equals('2016')); + expect(selectedDatum.sales, equals(100)); + expect(currentSelectionModel!.selectedSeries, hasLength(1)); + expect(currentSelectionModel!.selectedSeries.first.id, equals('Sales')); + }); +} + +class TestChart extends StatefulWidget { + final charts.SelectionModelListener selectionChangedListener; + final charts.UserManagedSelectionModel onTapSelection; + + TestChart(this.selectionChangedListener, this.onTapSelection); + + @override + TestChartState createState() { + return new TestChartState(selectionChangedListener, onTapSelection); + } +} + +class TestChartState extends State { + final charts.SelectionModelListener selectionChangedListener; + final charts.UserManagedSelectionModel onTapSelection; + + final seriesList = _createSampleData(); + final myState = new charts.UserManagedState(); + + TestChartState(this.selectionChangedListener, this.onTapSelection); + + @override + Widget build(BuildContext context) { + final chart = new charts.BarChart( + seriesList, + userManagedState: myState, + selectionModels: [ + new charts.SelectionModelConfig( + type: charts.SelectionModelType.info, + changedListener: widget.selectionChangedListener) + ], + // Disable animation and gesture for testing. + animate: false, //widget.animate, + defaultInteractions: false, + ); + + return new Directionality( + textDirection: TextDirection.ltr, + child: new GestureDetector(child: chart, onTap: handleOnTap), + ); + } + + void handleOnTap() { + setState(() { + myState.selectionModels[charts.SelectionModelType.info] = onTapSelection; + }); + } +} + +/// Create one series with sample hard coded data. +List> _createSampleData() { + final data = [ + new OrdinalSales('2014', 5), + new OrdinalSales('2015', 25), + new OrdinalSales('2016', 100), + new OrdinalSales('2017', 75), + ]; + + return [ + new charts.Series( + id: 'Sales', + colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault, + domainFn: (OrdinalSales sales, _) => sales.year, + measureFn: (OrdinalSales sales, _) => sales.sales, + data: data, + ) + ]; +} + +/// Sample ordinal data type. +class OrdinalSales { + final String year; + final int sales; + + OrdinalSales(this.year, this.sales); +} diff --git a/test/widget_layout_delegate_test.dart b/test/widget_layout_delegate_test.dart new file mode 100644 index 0000000..0c710f4 --- /dev/null +++ b/test/widget_layout_delegate_test.dart @@ -0,0 +1,561 @@ +// Copyright 2018 the Charts project authors. Please see the AUTHORS file +// for details. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math' show Rectangle; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:charts_common/common.dart' as common + show BehaviorPosition, InsideJustification, OutsideJustification; +import 'package:charts_flutter/src/behaviors/chart_behavior.dart'; +import 'package:charts_flutter/src/widget_layout_delegate.dart'; + +const chartContainerLayoutID = 'chartContainer'; + +// I couldn't get mockito to work with Widget return type, so fake it is. +class FakeBuildableBehavior implements BuildableBehavior { + common.BehaviorPosition position; + common.OutsideJustification outsideJustification; + common.InsideJustification insideJustification; + Rectangle? drawAreaBounds; + + FakeBuildableBehavior(this.position, this.outsideJustification, + this.insideJustification, this.drawAreaBounds) {} + + @override + Widget build(BuildContext context) { + throw UnimplementedError(); + } +} + +void main() { + group('widget layout test', () { + final chartKey = new UniqueKey(); + final behaviorKey = new UniqueKey(); + final behaviorID = 'behavior'; + final totalSize = const Size(200.0, 100.0); + final behaviorSize = const Size(50.0, 50.0); + + /// Creates widget for testing. + Widget createWidget( + Size chartSize, Size behaviorSize, common.BehaviorPosition position, + // Using these defaults, copied from DatumLegend. + {common.OutsideJustification outsideJustification = + common.OutsideJustification.startDrawArea, + common.InsideJustification insideJustification = + common.InsideJustification.topStart, + required Rectangle drawAreaBounds, + bool isRTL = false}) { + // Create a mock buildable behavior that returns information about the + // position and justification desired. + final behavior = new FakeBuildableBehavior( + position, outsideJustification, insideJustification, drawAreaBounds); + + // The 'chart' widget that expands to the full size allowed to test that + // the behavior widget's size affects the size given to the chart. + final chart = new LayoutId( + key: chartKey, id: chartContainerLayoutID, child: new Container()); + + // A behavior widget + final behaviorWidget = new LayoutId( + key: behaviorKey, + id: behaviorID, + child: new SizedBox.fromSize(size: behaviorSize)); + + // Create a the widget that uses the layout delegate that is being tested. + final layout = new CustomMultiChildLayout( + delegate: new WidgetLayoutDelegate( + chartContainerLayoutID, {behaviorID: behavior}, isRTL), + children: [chart, behaviorWidget]); + + final container = new Align( + alignment: Alignment.topLeft, + child: new Container( + width: chartSize.width, height: chartSize.height, child: layout)); + + return container; + } + + // Verifies the expected results. + void verifyResults(WidgetTester tester, Size expectedChartSize, + Offset expectedChartOffset, Offset expectedBehaviorOffset) { + final RenderBox chartBox = tester.firstRenderObject(find.byKey(chartKey)); + expect(chartBox.size, equals(expectedChartSize)); + + final chartOffset = chartBox.localToGlobal(Offset.zero); + expect(chartOffset, equals(expectedChartOffset)); + + final RenderBox behaviorBox = + tester.firstRenderObject(find.byKey(behaviorKey)); + final behaviorOffset = behaviorBox.localToGlobal(Offset.zero); + expect(behaviorOffset, equals(expectedBehaviorOffset)); + } + + testWidgets('Position top - start draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.startDrawArea; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to draw area + final expectedBehaviorOffset = const Offset(25.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position bottom - end draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.bottom; + final outsideJustification = common.OutsideJustification.endDrawArea; + final drawAreaBounds = const Rectangle(25, 0, 125, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the bottom, so the chart is offset by 0. + final expectedChartOffset = const Offset(0.0, 0.0); + // Behavior is aligned to draw area and offset to the bottom. + final expectedBehaviorOffset = const Offset(100.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position start - start draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.startDrawArea; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the start (left) since this is NOT a RTL + // so the chart is offset to the right by the behavior width of 50. + final expectedChartOffset = const Offset(50.0, 0.0); + // Behavior is aligned to draw area. + final expectedBehaviorOffset = const Offset(0.0, 25.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position end - end draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.end; + final outsideJustification = common.OutsideJustification.endDrawArea; + final drawAreaBounds = const Rectangle(25, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the right (left) since this is NOT a RTL + // so no offset for the chart. + final expectedChartOffset = const Offset(0.0, 0.0); + // Behavior is aligned to draw area and offset to the right of the + // chart. + final expectedBehaviorOffset = const Offset(150.0, 25.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position top - start justified', (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.start; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to the start, so no offset + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position top - end justified', (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.end; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to the end, so it is offset by total size minus + // the behavior size. + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position start - start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.start; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the start (left) since this is NOT a RTL + // so the chart is offset to the right by the behavior width of 50. + final expectedChartOffset = const Offset(50.0, 0.0); + // No offset because it is start justified. + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position start - end justified', (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.end; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the start (left) since this is NOT a RTL + // so the chart is offset to the right by the behavior width of 50. + final expectedChartOffset = const Offset(50.0, 0.0); + // End justified, total height minus behavior height + final expectedBehaviorOffset = const Offset(0.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position inside - top start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.inside; + final insideJustification = common.InsideJustification.topStart; + final drawAreaBounds = const Rectangle(25, 25, 175, 75); + + // Behavior is layered on top, chart uses the full size. + final expectedChartSize = const Size(200.0, 100.0); + // No offset since chart takes up full size. + final expectedChartOffset = const Offset(0.0, 0.0); + // Top start justified, no offset + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + insideJustification: insideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('Position inside - top end justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.inside; + final insideJustification = common.InsideJustification.topEnd; + final drawAreaBounds = const Rectangle(25, 25, 175, 75); + + // Behavior is layered on top, chart uses the full size. + final expectedChartSize = const Size(200.0, 100.0); + // No offset since chart takes up full size. + final expectedChartOffset = const Offset(0.0, 0.0); + // Offset to the top end + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + insideJustification: insideJustification, + drawAreaBounds: drawAreaBounds)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position top - start draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.startDrawArea; + final drawAreaBounds = const Rectangle(0, 50, 175, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to start draw area, which is to the left in RTL + final expectedBehaviorOffset = const Offset(125.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position bottom - end draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.bottom; + final outsideJustification = common.OutsideJustification.endDrawArea; + final drawAreaBounds = const Rectangle(0, 0, 175, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the bottom, so the chart is offset by 0. + final expectedChartOffset = const Offset(0.0, 0.0); + // Behavior is aligned to end draw area (left) and offset to the bottom. + final expectedBehaviorOffset = const Offset(0.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position start - start draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.startDrawArea; + final drawAreaBounds = const Rectangle(0, 25, 125, 75); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Chart is on the left, so no offset. + final expectedChartOffset = const Offset(0.0, 0.0); + // Behavior is positioned at the start (right) and start draw area. + final expectedBehaviorOffset = const Offset(150.0, 25.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position end - end draw area justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.end; + final outsideJustification = common.OutsideJustification.endDrawArea; + final drawAreaBounds = const Rectangle(75, 25, 125, 75); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Chart is to the left of the behavior because of RTL. + final expectedChartOffset = const Offset(50.0, 0.0); + // Behavior is aligned to end draw area. + final expectedBehaviorOffset = const Offset(0.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position top - start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.start; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to the end, offset by behavior size. + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position top - end justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.top; + final outsideJustification = common.OutsideJustification.end; + final drawAreaBounds = const Rectangle(25, 50, 150, 50); + + // Behavior takes up 50 height, so 50 height remains for the chart. + final expectedChartSize = const Size(200.0, 50.0); + // Behavior is positioned on the top, so the chart is offset by 50. + final expectedChartOffset = const Offset(0.0, 50.0); + // Behavior is aligned to the end, no offset. + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position start - start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.start; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the right since this is RTL so the chart is + // has no offset. + final expectedChartOffset = const Offset(0.0, 0.0); + // No offset because it is start justified. + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position start - end justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.start; + final outsideJustification = common.OutsideJustification.end; + final drawAreaBounds = const Rectangle(75, 25, 150, 50); + + // Behavior takes up 50 width, so 150 width remains for the chart. + final expectedChartSize = const Size(150.0, 100.0); + // Behavior is positioned at the right since this is RTL so the chart is + // has no offset. + final expectedChartOffset = const Offset(0.0, 0.0); + // End justified, total height minus behavior height + final expectedBehaviorOffset = const Offset(150.0, 50.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + outsideJustification: outsideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position inside - top start justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.inside; + final insideJustification = common.InsideJustification.topStart; + final drawAreaBounds = const Rectangle(25, 25, 175, 75); + + // Behavior is layered on top, chart uses the full size. + final expectedChartSize = const Size(200.0, 100.0); + // No offset since chart takes up full size. + final expectedChartOffset = const Offset(0.0, 0.0); + // Offset to the right + final expectedBehaviorOffset = const Offset(150.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + insideJustification: insideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + + testWidgets('RTL - Position inside - top end justified', + (WidgetTester tester) async { + final behaviorPosition = common.BehaviorPosition.inside; + final insideJustification = common.InsideJustification.topEnd; + final drawAreaBounds = const Rectangle(25, 25, 175, 75); + + // Behavior is layered on top, chart uses the full size. + final expectedChartSize = const Size(200.0, 100.0); + // No offset since chart takes up full size. + final expectedChartOffset = const Offset(0.0, 0.0); + // No offset, since end is to the left. + final expectedBehaviorOffset = const Offset(0.0, 0.0); + + await tester.pumpWidget(createWidget( + totalSize, behaviorSize, behaviorPosition, + insideJustification: insideJustification, + drawAreaBounds: drawAreaBounds, + isRTL: true)); + + verifyResults(tester, expectedChartSize, expectedChartOffset, + expectedBehaviorOffset); + }); + }); +}